-
# frozen_string_literal: true
-
1
require_relative 'app_base'
-
1
require_relative 'probe'
-
1
require_relative 'helpers/app_helpers'
-
-
1
class App < AppBase
-
-
1
def initialize(externals)
-
2
super()
-
2
@externals = externals
-
end
-
-
1
attr_reader :externals
-
-
1
get_probe(:alive?) # curl/k8s
-
1
get_probe(:ready?) # curl/k8s
-
1
get_probe(:sha) # identity
-
-
1
def probe
-
Probe.new(externals)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
get '/index', provides:[:html] do
-
respond_to do |format|
-
format.html do
-
erb :index
-
end
-
end
-
end
-
-
1
helpers AppHelpers
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'silently'
-
1
require 'sinatra/base'
-
2
silently { require 'sinatra/contrib' } # N x "warning: method redefined"
-
1
require_relative 'http_json_hash/service'
-
1
require 'json'
-
1
require 'sprockets'
-
1
require 'uglifier'
-
-
1
class AppBase < Sinatra::Base
-
-
1
def initialize
-
2
super(nil)
-
end
-
-
2
silently { register Sinatra::Contrib }
-
1
set :port, ENV['PORT']
-
1
set :environment, Sprockets::Environment.new
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
environment.append_path('code/assets/stylesheets')
-
1
environment.css_compressor = :sassc
-
-
1
get '/assets/app.css', provides:[:css] do
-
1
respond_to do |format|
-
1
format.css do
-
1
env['PATH_INFO'].sub!('/assets', '')
-
1
settings.environment.call(env)
-
end
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
environment.append_path('code/assets/javascripts')
-
1
environment.js_compressor = Uglifier.new(harmony: true)
-
-
1
get '/assets/app.js', provides:[:js] do
-
1
respond_to do |format|
-
1
format.js do
-
1
env['PATH_INFO'].sub!('/assets', '')
-
1
settings.environment.call(env)
-
end
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def self.get_probe(name)
-
3
get "/#{name}", provides:[:json] do
-
result = instance_exec {
-
probe.public_send(name)
-
}
-
json({ name => result })
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def json_args
-
symbolized(json_payload)
-
end
-
-
1
private
-
-
1
def symbolized(h)
-
# named-args require symbolization
-
Hash[h.map{ |key,value| [key.to_sym, value] }]
-
end
-
-
1
def json_payload
-
json_hash_parse(request.body.read)
-
end
-
-
1
def json_hash_parse(body)
-
json = (body === '') ? {} : JSON.parse!(body)
-
unless json.instance_of?(Hash)
-
fail 'body is not JSON Hash'
-
end
-
json
-
rescue JSON::ParserError
-
fail 'body is not JSON'
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
set :show_exceptions, false
-
-
1
error do
-
error = $!
-
status(500)
-
content_type('application/json')
-
info = {
-
exception: {
-
request: {
-
path:request.path,
-
body:request.body.read
-
},
-
backtrace: error.backtrace
-
}
-
}
-
exception = info[:exception]
-
if error.instance_of?(::HttpJsonHash::ServiceError)
-
exception[:http_service] = {
-
path:error.path,
-
args:error.args,
-
name:error.name,
-
body:error.body,
-
message:error.message
-
}
-
else
-
exception[:message] = error.message
-
end
-
diagnostic = JSON.pretty_generate(info)
-
puts diagnostic
-
body diagnostic
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'http_json_hash/service'
-
-
1
class ExternalAvatars
-
-
1
def initialize(http)
-
@http = HttpJsonHash::service(self.class.name, http, 'avatars', 5027)
-
end
-
-
1
def ready?
-
@http.get(__method__, {})
-
end
-
-
1
def names
-
@http.get(__method__, {})
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require 'net/http'
-
-
1
class ExternalHttp
-
-
1
def get(uri)
-
KLASS::Get.new(uri)
-
end
-
-
1
def post(uri)
-
KLASS::Post.new(uri)
-
end
-
-
1
def start(hostname, port, req)
-
KLASS.start(hostname, port) do |http|
-
http.request(req)
-
end
-
end
-
-
1
private
-
-
1
KLASS = Net::HTTP
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'http_json_hash/service'
-
-
1
class ExternalSaver
-
-
1
def initialize(http)
-
@http = HttpJsonHash::service(self.class.name, http, 'saver', 4537)
-
end
-
-
1
def ready?
-
@http.get(__method__, {})
-
end
-
-
# - - - - - - - - - - - - - - - - - - -
-
-
1
def dir_make_command(dirname)
-
['dir_make',dirname]
-
end
-
-
1
def dir_exists_command(dirname)
-
['dir_exists?',dirname]
-
end
-
-
1
def file_create_command(filename, content)
-
['file_create',filename,content]
-
end
-
-
1
def file_read_command(filename)
-
['file_read',filename]
-
end
-
-
# - - - - - - - - - - - - - - - - - - -
-
# primitives
-
-
1
def assert(command)
-
@http.post(__method__, { command:command })
-
end
-
-
# - - - - - - - - - - - - - - - - - - -
-
# batches
-
-
1
def assert_all(commands)
-
@http.post(__method__, { commands:commands })
-
end
-
-
1
def run_all(commands)
-
@http.post(__method__, { commands:commands })
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'external_avatars'
-
1
require_relative 'external_http'
-
1
require_relative 'external_saver'
-
-
1
class Externals
-
-
1
def avatars
-
@avatars ||= ExternalAvatars.new(avatars_http)
-
end
-
1
def avatars_http
-
@avatars_http ||= ExternalHttp.new
-
end
-
-
1
def saver
-
@saver ||= ExternalSaver.new(saver_http)
-
end
-
1
def saver_http
-
@saver_http ||= ExternalHttp.new
-
end
-
-
end
-
#require_relative 'diff_avatar_image'
-
-
1
module AppHelpers
-
-
#core, eg groups,kata
-
-
end
-
# frozen_string_literal: true
-
1
require 'json'
-
1
require 'uri'
-
-
1
module HttpJsonHash
-
1
class Requester
-
-
1
def initialize(http, hostname, port)
-
@http = http
-
@hostname = hostname
-
@port = port
-
end
-
-
1
def get(path, args)
-
request(path, args) do |uri|
-
@http.get(uri)
-
end
-
end
-
-
1
def post(path, args)
-
request(path, args) do |uri|
-
@http.post(uri)
-
end
-
end
-
-
1
private
-
-
1
def request(path, args)
-
uri = URI.parse("http://#{@hostname}:#{@port}/#{path}")
-
req = yield uri
-
req.content_type = 'application/json'
-
req.body = JSON.fast_generate(args)
-
@http.start(@hostname, @port, req)
-
end
-
-
end
-
end
-
# frozen_string_literal: true
-
1
require_relative 'requester'
-
1
require_relative 'unpacker'
-
-
1
module HttpJsonHash
-
-
1
def self.service(name, http, hostname, port)
-
requester = Requester.new(http, hostname, port)
-
Unpacker.new(name, requester)
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
module HttpJsonHash
-
1
class ServiceError < RuntimeError
-
1
def initialize(path, args, name, body, message)
-
@path = path
-
@args = args
-
@name = name
-
@body = body
-
super(message)
-
end
-
1
attr_reader :path, :args, :name, :body
-
end
-
end
-
# frozen_string_literal: true
-
1
require_relative 'service_error'
-
1
require 'json'
-
-
1
module HttpJsonHash
-
1
class Unpacker
-
-
1
def initialize(name, requester)
-
@name = name
-
@requester = requester
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
def get(path, args)
-
response = @requester.get(path, args)
-
unpacked(response.body, path.to_s, args)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
def post(path, args)
-
response = @requester.post(path, args)
-
unpacked(response.body, path.to_s, args)
-
end
-
-
1
private
-
-
1
def unpacked(body, path, args)
-
json = JSON.parse!(body)
-
unless json.instance_of?(Hash)
-
service_error(path, args, body, 'body is not JSON Hash')
-
end
-
if json.has_key?('exception')
-
service_error(path, args, body, 'body has embedded exception')
-
end
-
unless json.has_key?(path)
-
service_error(path, args, body, 'body is missing :path key')
-
end
-
json[path]
-
rescue JSON::ParserError
-
service_error(path, args, body, 'body is not JSON')
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
def service_error(path, args, body, message)
-
fail ::HttpJsonHash::ServiceError.new(path, args, @name, body, message)
-
end
-
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Probe
-
-
1
def initialize(externals)
-
@externals = externals
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
# k8s/curl probing + identity
-
-
1
def alive?
-
true
-
end
-
-
1
def ready?
-
dependent_services.all?(&:ready?)
-
end
-
-
1
def sha
-
ENV['SHA']
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
private
-
-
1
def dependent_services
-
[ saver ]
-
end
-
-
1
def saver
-
@externals.saver
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
def silently
-
2
old_stderr = $stderr
-
2
$stderr = StringIO.new
-
2
yield
-
ensure
-
2
$stderr = old_stderr
-
end
-
1
require 'minitest/autorun'
-
1
require 'rack/test'
-
-
1
def require_source(required)
-
2
require_relative "../app/code/#{required}"
-
end
-
-
1
class Id58TestBase < MiniTest::Test
-
-
1
def initialize(arg)
-
3
@id58 = nil
-
3
@name58 = nil
-
3
super
-
end
-
-
1
@@args = (ARGV.sort.uniq - ['--']) # eg 2m4
-
1
@@seen_ids = []
-
1
@@timings = {}
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def self.test(id58_suffix, *lines, &test_block)
-
3
source = test_block.source_location
-
3
source_file = File.basename(source[0])
-
3
source_line = source[1].to_s
-
3
id58 = checked_id58(id58_suffix.to_s, lines)
-
3
if @@args === [] || @@args.any?{ |arg| id58.include?(arg) }
-
3
name58 = lines.join(space = ' ')
-
3
execute_around = lambda {
-
3
ENV['ID58'] = id58
-
3
@id58 = id58
-
3
@name58 = name58
-
3
id58_setup
-
begin
-
3
t1 = Time.now
-
3
self.instance_exec(&test_block)
-
3
t2 = Time.now
-
3
stripped = trimmed(name58.split("\n").join)
-
3
@@timings[id58+':'+source_file+':'+source_line+':'+stripped] = (t2 - t1)
-
ensure
-
3
puts $!.message unless $!.nil?
-
3
id58_teardown
-
end
-
}
-
3
name = "#{id58_suffix}:#{name58}"
-
3
define_method("test_\n\n#{name}".to_sym, &execute_around)
-
end
-
end
-
-
1
def trimmed(s)
-
3
if s.length > 80
-
s[0..80] + '...'
-
else
-
3
s
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
skipped
# :nocov:
-
skipped
ObjectSpace.define_finalizer(self, proc {
-
skipped
slow = @@timings.select{ |_name,secs| secs > 0.000 }
-
skipped
sorted = slow.sort_by{ |name,secs| -secs }.to_h
-
skipped
size = sorted.size < 5 ? sorted.size : 5
-
skipped
puts
-
skipped
puts "Slowest #{size} tests are..." if size != 0
-
skipped
sorted.each_with_index { |(name,secs),index|
-
skipped
puts "%3.4f - %-72s" % [secs,name]
-
skipped
break if index === size
-
skipped
}
-
skipped
puts
-
skipped
})
-
skipped
# :nocov:
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
ID58_ALPHABET = %w{
-
1
0 1 2 3 4 5 6 7 8 9
-
A B C D E F G H J K L M N P Q R S T U V W X Y Z
-
a b c d e f g h j k l m n p q r s t u v w x y z
-
}.join.freeze
-
-
1
def self.id58?(s)
-
6
s.instance_of?(String) &&
-
18
s.chars.all?{ |ch| ID58_ALPHABET.include?(ch) }
-
end
-
-
1
def self.checked_id58(id58_suffix, lines)
-
3
method = 'def self.id58_prefix'
-
3
pointer = ' ' * method.index('.') + '!'
-
3
pointee = (['',pointer,method,'','']).join("\n")
-
3
pointer.prepend("\n\n")
-
3
raise "#{pointer}missing#{pointee}" unless respond_to?(:id58_prefix)
-
3
prefix = id58_prefix.to_s
-
3
raise "#{pointer}empty#{pointee}" if prefix === ''
-
3
raise "#{pointer}not id58#{pointee}" unless id58?(prefix)
-
-
3
method = "test '#{id58_suffix}',"
-
3
pointer = ' ' * method.index("'") + '!'
-
3
proposition = lines.join(space = ' ')
-
3
pointee = ['',pointer,method,"'#{proposition}'",'',''].join("\n")
-
3
id58 = prefix + id58_suffix
-
3
pointer.prepend("\n\n")
-
3
raise "#{pointer}empty#{pointee}" if id58_suffix === ''
-
3
raise "#{pointer}not id58#{pointee}" unless id58?(id58_suffix)
-
3
raise "#{pointer}duplicate#{pointee}" if @@seen_ids.include?(id58)
-
3
raise "#{pointer}overlap#{pointee}" if prefix[-2..-1] === id58_suffix[0..1]
-
3
@@seen_ids << id58
-
3
id58
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def id58_setup
-
end
-
-
1
def id58_teardown
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def id58
-
@id58
-
end
-
-
1
def name58
-
@name58
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class AssetsTest < TestBase
-
-
1
def self.id58_prefix
-
2
'Q3p'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '2Je', %w(
-
|GET /assets/app.css is served
-
) do
-
1
get '/assets/app.css'
-
1
assert status?(200), status
-
1
assert css_content?, content_type
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '3Je', %w(
-
|GET /assets/app.js is served
-
) do
-
1
get '/assets/app.js'
-
1
assert status?(200), status
-
1
assert js_content?, content_type
-
end
-
-
end
-
1
require_relative 'test_base'
-
-
1
class ExampleTest < TestBase
-
-
1
def self.id58_prefix
-
1
'449'
-
end
-
-
#- - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def id58_setup
-
end
-
-
#- - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
test 'AC6', %w( example ) do
-
1
assert_equal 42, 42
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative '../id58_test_base'
-
1
require_source 'app'
-
1
require_source 'externals'
-
-
1
class TestBase < Id58TestBase
-
-
1
include Rack::Test::Methods #Â [1]
-
-
1
def app #Â [1]
-
2
App.new(externals)
-
end
-
-
1
def externals
-
2
@externals ||= Externals.new
-
end
-
-
# - - - - - - - - - - - - - - -
-
-
1
def status?(expected)
-
2
status === expected
-
end
-
-
1
def status
-
4
last_response.status
-
end
-
-
# - - - - - - - - - - - - - - -
-
-
1
def html_content?
-
content_type === 'text/html;charset=utf-8'
-
end
-
-
1
def css_content?
-
1
content_type === 'text/css; charset=utf-8'
-
end
-
-
1
def json_content?
-
content_type === 'application/json'
-
end
-
-
1
def js_content?
-
1
content_type === 'application/javascript'
-
end
-
-
1
def content_type
-
4
last_response.headers['Content-Type']
-
end
-
-
end
-
1
require 'backports/tools/deprecation'
-
-
1
Backports.deprecate :rails, 'Rails backports are deprecated.'
-
-
1
class String
-
# Standard in rails. See official documentation[http://api.rubyonrails.org/classes/ActiveSupport/CoreExtensions/String/Inflections.html]
-
def camelize(first_letter = :upper)
-
if first_letter == :upper
-
gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
-
else
-
self[0..0].downcase + camelize[1..-1]
-
end
-
1
end unless method_defined? :camelize
-
-
# Standard in rails. See official documentation[http://api.rubyonrails.org/classes/ActiveSupport/CoreExtensions/String/Inflections.html]
-
def constantize
-
names = split('::')
-
names.shift if names.empty? || names.first.empty?
-
-
constant = Object
-
names.each do |name|
-
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
-
end
-
constant
-
1
end unless method_defined? :constantize
-
-
# Standard in rails. See official documentation[http://api.rubyonrails.org/classes/ActiveSupport/CoreExtensions/String/Inflections.html]
-
def dasherize
-
gsub(/_/, '-')
-
1
end unless method_defined? :dasherize
-
-
# Standard in rails. See official documentation[http://api.rubyonrails.org/classes/ActiveSupport/CoreExtensions/String/Inflections.html]
-
def demodulize
-
gsub(/^.*::/, '')
-
1
end unless method_defined? :demodulize
-
-
# Standard in rails. See official documentation[http://api.rubyonrails.org/classes/ActiveSupport/CoreExtensions/String/Inflections.html]
-
def underscore
-
15
gsub(/::/, '/').
-
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
-
gsub(/([a-z\d])([A-Z])/,'\1_\2').
-
tr("-", "_").
-
downcase
-
1
end unless method_defined? :underscore
-
-
end
-
1
module Backports
-
1
class << self
-
1
attr_accessor :warned # private
-
1
Backports.warned = {}
-
-
1
def frown_upon kind, msg
-
warn kind, msg if $VERBOSE
-
end
-
-
1
def warn kind, msg
-
1
return if warned[kind]
-
1
super msg
-
1
warned[kind] = msg
-
end
-
-
1
alias_method :deprecate, :warn
-
-
end
-
end
-
1
require 'concurrent/version'
-
1
require 'concurrent/constants'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/configuration'
-
-
1
require 'concurrent/atomics'
-
1
require 'concurrent/executors'
-
1
require 'concurrent/synchronization'
-
-
1
require 'concurrent/atomic/atomic_markable_reference'
-
1
require 'concurrent/atomic/atomic_reference'
-
1
require 'concurrent/agent'
-
1
require 'concurrent/atom'
-
1
require 'concurrent/array'
-
1
require 'concurrent/hash'
-
1
require 'concurrent/set'
-
1
require 'concurrent/map'
-
1
require 'concurrent/tuple'
-
1
require 'concurrent/async'
-
1
require 'concurrent/dataflow'
-
1
require 'concurrent/delay'
-
1
require 'concurrent/exchanger'
-
1
require 'concurrent/future'
-
1
require 'concurrent/immutable_struct'
-
1
require 'concurrent/ivar'
-
1
require 'concurrent/maybe'
-
1
require 'concurrent/mutable_struct'
-
1
require 'concurrent/mvar'
-
1
require 'concurrent/promise'
-
1
require 'concurrent/scheduled_task'
-
1
require 'concurrent/settable_struct'
-
1
require 'concurrent/timer_task'
-
1
require 'concurrent/tvar'
-
1
require 'concurrent/promises'
-
-
1
require 'concurrent/thread_safe/synchronized_delegator'
-
1
require 'concurrent/thread_safe/util'
-
-
1
require 'concurrent/options'
-
-
# @!macro internal_implementation_note
-
#
-
# @note **Private Implementation:** This abstraction is a private, internal
-
# implementation detail. It should never be used directly.
-
-
# @!macro monotonic_clock_warning
-
#
-
# @note Time calculations on all platforms and languages are sensitive to
-
# changes to the system clock. To alleviate the potential problems
-
# associated with changing the system clock while an application is running,
-
# most modern operating systems provide a monotonic clock that operates
-
# independently of the system clock. A monotonic clock cannot be used to
-
# determine human-friendly clock times. A monotonic clock is used exclusively
-
# for calculating time intervals. Not all Ruby platforms provide access to an
-
# operating system monotonic clock. On these platforms a pure-Ruby monotonic
-
# clock will be used as a fallback. An operating system monotonic clock is both
-
# faster and more reliable than the pure-Ruby implementation. The pure-Ruby
-
# implementation should be fast and reliable enough for most non-realtime
-
# operations. At this time the common Ruby platforms that provide access to an
-
# operating system monotonic clock are MRI 2.1 and above and JRuby (all versions).
-
#
-
# @see http://linux.die.net/man/3/clock_gettime Linux clock_gettime(3)
-
-
# @!macro copy_options
-
#
-
# ## Copy Options
-
#
-
# Object references in Ruby are mutable. This can lead to serious
-
# problems when the {#value} of an object is a mutable reference. Which
-
# is always the case unless the value is a `Fixnum`, `Symbol`, or similar
-
# "primitive" data type. Each instance can be configured with a few
-
# options that can help protect the program from potentially dangerous
-
# operations. Each of these options can be optionally set when the object
-
# instance is created:
-
#
-
# * `:dup_on_deref` When true the object will call the `#dup` method on
-
# the `value` object every time the `#value` method is called
-
# (default: false)
-
# * `:freeze_on_deref` When true the object will call the `#freeze`
-
# method on the `value` object every time the `#value` method is called
-
# (default: false)
-
# * `:copy_on_deref` When given a `Proc` object the `Proc` will be run
-
# every time the `#value` method is called. The `Proc` will be given
-
# the current `value` as its only argument and the result returned by
-
# the block will be the return value of the `#value` call. When `nil`
-
# this option will be ignored (default: nil)
-
#
-
# When multiple deref options are set the order of operations is strictly defined.
-
# The order of deref operations is:
-
# * `:copy_on_deref`
-
# * `:dup_on_deref`
-
# * `:freeze_on_deref`
-
#
-
# Because of this ordering there is no need to `#freeze` an object created by a
-
# provided `:copy_on_deref` block. Simply set `:freeze_on_deref` to `true`.
-
# Setting both `:dup_on_deref` to `true` and `:freeze_on_deref` to `true` is
-
# as close to the behavior of a "pure" functional language (like Erlang, Clojure,
-
# or Haskell) as we are likely to get in Ruby.
-
-
# @!macro deref_options
-
#
-
# @option opts [Boolean] :dup_on_deref (false) Call `#dup` before
-
# returning the data from {#value}
-
# @option opts [Boolean] :freeze_on_deref (false) Call `#freeze` before
-
# returning the data from {#value}
-
# @option opts [Proc] :copy_on_deref (nil) When calling the {#value}
-
# method, call the given proc passing the internal value as the sole
-
# argument then return the new value returned from the proc.
-
-
# @!macro executor_and_deref_options
-
#
-
# @param [Hash] opts the options used to define the behavior at update and deref
-
# and to specify the executor on which to perform actions
-
# @option opts [Executor] :executor when set use the given `Executor` instance.
-
# Three special values are also supported: `:io` returns the global pool for
-
# long, blocking (IO) tasks, `:fast` returns the global pool for short, fast
-
# operations, and `:immediate` returns the global `ImmediateExecutor` object.
-
# @!macro deref_options
-
-
# @!macro warn.edge
-
# @api Edge
-
# @note **Edge Features** are under active development and may change frequently.
-
#
-
# - Deprecations are not added before incompatible changes.
-
# - Edge version: _major_ is always 0, _minor_ bump means incompatible change,
-
# _patch_ bump means compatible change.
-
# - Edge features may also lack tests and documentation.
-
# - Features developed in `concurrent-ruby-edge` are expected to move
-
# to `concurrent-ruby` when finalised.
-
-
-
# {include:file:README.md}
-
1
module Concurrent
-
end
-
1
require 'concurrent/configuration'
-
1
require 'concurrent/atomic/atomic_reference'
-
1
require 'concurrent/atomic/thread_local_var'
-
1
require 'concurrent/collection/copy_on_write_observer_set'
-
1
require 'concurrent/concern/observable'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# `Agent` is inspired by Clojure's [agent](http://clojure.org/agents)
-
# function. An agent is a shared, mutable variable providing independent,
-
# uncoordinated, *asynchronous* change of individual values. Best used when
-
# the value will undergo frequent, complex updates. Suitable when the result
-
# of an update does not need to be known immediately. `Agent` is (mostly)
-
# functionally equivalent to Clojure's agent, except where the runtime
-
# prevents parity.
-
#
-
# Agents are reactive, not autonomous - there is no imperative message loop
-
# and no blocking receive. The state of an Agent should be itself immutable
-
# and the `#value` of an Agent is always immediately available for reading by
-
# any thread without any messages, i.e. observation does not require
-
# cooperation or coordination.
-
#
-
# Agent action dispatches are made using the various `#send` methods. These
-
# methods always return immediately. At some point later, in another thread,
-
# the following will happen:
-
#
-
# 1. The given `action` will be applied to the state of the Agent and the
-
# `args`, if any were supplied.
-
# 2. The return value of `action` will be passed to the validator lambda,
-
# if one has been set on the Agent.
-
# 3. If the validator succeeds or if no validator was given, the return value
-
# of the given `action` will become the new `#value` of the Agent. See
-
# `#initialize` for details.
-
# 4. If any observers were added to the Agent, they will be notified. See
-
# `#add_observer` for details.
-
# 5. If during the `action` execution any other dispatches are made (directly
-
# or indirectly), they will be held until after the `#value` of the Agent
-
# has been changed.
-
#
-
# If any exceptions are thrown by an action function, no nested dispatches
-
# will occur, and the exception will be cached in the Agent itself. When an
-
# Agent has errors cached, any subsequent interactions will immediately throw
-
# an exception, until the agent's errors are cleared. Agent errors can be
-
# examined with `#error` and the agent restarted with `#restart`.
-
#
-
# The actions of all Agents get interleaved amongst threads in a thread pool.
-
# At any point in time, at most one action for each Agent is being executed.
-
# Actions dispatched to an agent from another single agent or thread will
-
# occur in the order they were sent, potentially interleaved with actions
-
# dispatched to the same agent from other sources. The `#send` method should
-
# be used for actions that are CPU limited, while the `#send_off` method is
-
# appropriate for actions that may block on IO.
-
#
-
# Unlike in Clojure, `Agent` cannot participate in `Concurrent::TVar` transactions.
-
#
-
# ## Example
-
#
-
# ```
-
# def next_fibonacci(set = nil)
-
# return [0, 1] if set.nil?
-
# set + [set[-2..-1].reduce{|sum,x| sum + x }]
-
# end
-
#
-
# # create an agent with an initial value
-
# agent = Concurrent::Agent.new(next_fibonacci)
-
#
-
# # send a few update requests
-
# 5.times do
-
# agent.send{|set| next_fibonacci(set) }
-
# end
-
#
-
# # wait for them to complete
-
# agent.await
-
#
-
# # get the current value
-
# agent.value #=> [0, 1, 1, 2, 3, 5, 8]
-
# ```
-
#
-
# ## Observation
-
#
-
# Agents support observers through the {Concurrent::Observable} mixin module.
-
# Notification of observers occurs every time an action dispatch returns and
-
# the new value is successfully validated. Observation will *not* occur if the
-
# action raises an exception, if validation fails, or when a {#restart} occurs.
-
#
-
# When notified the observer will receive three arguments: `time`, `old_value`,
-
# and `new_value`. The `time` argument is the time at which the value change
-
# occurred. The `old_value` is the value of the Agent when the action began
-
# processing. The `new_value` is the value to which the Agent was set when the
-
# action completed. Note that `old_value` and `new_value` may be the same.
-
# This is not an error. It simply means that the action returned the same
-
# value.
-
#
-
# ## Nested Actions
-
#
-
# It is possible for an Agent action to post further actions back to itself.
-
# The nested actions will be enqueued normally then processed *after* the
-
# outer action completes, in the order they were sent, possibly interleaved
-
# with action dispatches from other threads. Nested actions never deadlock
-
# with one another and a failure in a nested action will never affect the
-
# outer action.
-
#
-
# Nested actions can be called using the Agent reference from the enclosing
-
# scope or by passing the reference in as a "send" argument. Nested actions
-
# cannot be post using `self` from within the action block/proc/lambda; `self`
-
# in this context will not reference the Agent. The preferred method for
-
# dispatching nested actions is to pass the Agent as an argument. This allows
-
# Ruby to more effectively manage the closing scope.
-
#
-
# Prefer this:
-
#
-
# ```
-
# agent = Concurrent::Agent.new(0)
-
# agent.send(agent) do |value, this|
-
# this.send {|v| v + 42 }
-
# 3.14
-
# end
-
# agent.value #=> 45.14
-
# ```
-
#
-
# Over this:
-
#
-
# ```
-
# agent = Concurrent::Agent.new(0)
-
# agent.send do |value|
-
# agent.send {|v| v + 42 }
-
# 3.14
-
# end
-
# ```
-
#
-
# @!macro agent_await_warning
-
#
-
# **NOTE** Never, *under any circumstances*, call any of the "await" methods
-
# ({#await}, {#await_for}, {#await_for!}, and {#wait}) from within an action
-
# block/proc/lambda. The call will block the Agent and will always fail.
-
# Calling either {#await} or {#wait} (with a timeout of `nil`) will
-
# hopelessly deadlock the Agent with no possibility of recovery.
-
#
-
# @!macro thread_safe_variable_comparison
-
#
-
# @see http://clojure.org/Agents Clojure Agents
-
# @see http://clojure.org/state Values and Change - Clojure's approach to Identity and State
-
1
class Agent < Synchronization::LockableObject
-
1
include Concern::Observable
-
-
1
ERROR_MODES = [:continue, :fail].freeze
-
1
private_constant :ERROR_MODES
-
-
1
AWAIT_FLAG = ::Object.new
-
1
private_constant :AWAIT_FLAG
-
-
1
AWAIT_ACTION = ->(value, latch) { latch.count_down; AWAIT_FLAG }
-
1
private_constant :AWAIT_ACTION
-
-
1
DEFAULT_ERROR_HANDLER = ->(agent, error) { nil }
-
1
private_constant :DEFAULT_ERROR_HANDLER
-
-
1
DEFAULT_VALIDATOR = ->(value) { true }
-
1
private_constant :DEFAULT_VALIDATOR
-
-
1
Job = Struct.new(:action, :args, :executor, :caller)
-
1
private_constant :Job
-
-
# Raised during action processing or any other time in an Agent's lifecycle.
-
1
class Error < StandardError
-
1
def initialize(message = nil)
-
message ||= 'agent must be restarted before jobs can post'
-
super(message)
-
end
-
end
-
-
# Raised when a new value obtained during action processing or at `#restart`
-
# fails validation.
-
1
class ValidationError < Error
-
1
def initialize(message = nil)
-
message ||= 'invalid value'
-
super(message)
-
end
-
end
-
-
# The error mode this Agent is operating in. See {#initialize} for details.
-
1
attr_reader :error_mode
-
-
# Create a new `Agent` with the given initial value and options.
-
#
-
# The `:validator` option must be `nil` or a side-effect free proc/lambda
-
# which takes one argument. On any intended value change the validator, if
-
# provided, will be called. If the new value is invalid the validator should
-
# return `false` or raise an error.
-
#
-
# The `:error_handler` option must be `nil` or a proc/lambda which takes two
-
# arguments. When an action raises an error or validation fails, either by
-
# returning false or raising an error, the error handler will be called. The
-
# arguments to the error handler will be a reference to the agent itself and
-
# the error object which was raised.
-
#
-
# The `:error_mode` may be either `:continue` (the default if an error
-
# handler is given) or `:fail` (the default if error handler nil or not
-
# given).
-
#
-
# If an action being run by the agent throws an error or doesn't pass
-
# validation the error handler, if present, will be called. After the
-
# handler executes if the error mode is `:continue` the Agent will continue
-
# as if neither the action that caused the error nor the error itself ever
-
# happened.
-
#
-
# If the mode is `:fail` the Agent will become {#failed?} and will stop
-
# accepting new action dispatches. Any previously queued actions will be
-
# held until {#restart} is called. The {#value} method will still work,
-
# returning the value of the Agent before the error.
-
#
-
# @param [Object] initial the initial value
-
# @param [Hash] opts the configuration options
-
#
-
# @option opts [Symbol] :error_mode either `:continue` or `:fail`
-
# @option opts [nil, Proc] :error_handler the (optional) error handler
-
# @option opts [nil, Proc] :validator the (optional) validation procedure
-
1
def initialize(initial, opts = {})
-
super()
-
synchronize { ns_initialize(initial, opts) }
-
end
-
-
# The current value (state) of the Agent, irrespective of any pending or
-
# in-progress actions. The value is always available and is non-blocking.
-
#
-
# @return [Object] the current value
-
1
def value
-
@current.value # TODO (pitr 12-Sep-2015): broken unsafe read?
-
end
-
-
1
alias_method :deref, :value
-
-
# When {#failed?} and {#error_mode} is `:fail`, returns the error object
-
# which caused the failure, else `nil`. When {#error_mode} is `:continue`
-
# will *always* return `nil`.
-
#
-
# @return [nil, Error] the error which caused the failure when {#failed?}
-
1
def error
-
@error.value
-
end
-
-
1
alias_method :reason, :error
-
-
# @!macro agent_send
-
#
-
# Dispatches an action to the Agent and returns immediately. Subsequently,
-
# in a thread from a thread pool, the {#value} will be set to the return
-
# value of the action. Action dispatches are only allowed when the Agent
-
# is not {#failed?}.
-
#
-
# The action must be a block/proc/lambda which takes 1 or more arguments.
-
# The first argument is the current {#value} of the Agent. Any arguments
-
# passed to the send method via the `args` parameter will be passed to the
-
# action as the remaining arguments. The action must return the new value
-
# of the Agent.
-
#
-
# * {#send} and {#send!} should be used for actions that are CPU limited
-
# * {#send_off}, {#send_off!}, and {#<<} are appropriate for actions that
-
# may block on IO
-
# * {#send_via} and {#send_via!} are used when a specific executor is to
-
# be used for the action
-
#
-
# @param [Array<Object>] args zero or more arguments to be passed to
-
# the action
-
# @param [Proc] action the action dispatch to be enqueued
-
#
-
# @yield [agent, value, *args] process the old value and return the new
-
# @yieldparam [Object] value the current {#value} of the Agent
-
# @yieldparam [Array<Object>] args zero or more arguments to pass to the
-
# action
-
# @yieldreturn [Object] the new value of the Agent
-
#
-
# @!macro send_return
-
# @return [Boolean] true if the action is successfully enqueued, false if
-
# the Agent is {#failed?}
-
1
def send(*args, &action)
-
enqueue_action_job(action, args, Concurrent.global_fast_executor)
-
end
-
-
# @!macro agent_send
-
#
-
# @!macro send_bang_return_and_raise
-
# @return [Boolean] true if the action is successfully enqueued
-
# @raise [Concurrent::Agent::Error] if the Agent is {#failed?}
-
1
def send!(*args, &action)
-
raise Error.new unless send(*args, &action)
-
true
-
end
-
-
# @!macro agent_send
-
# @!macro send_return
-
1
def send_off(*args, &action)
-
enqueue_action_job(action, args, Concurrent.global_io_executor)
-
end
-
-
1
alias_method :post, :send_off
-
-
# @!macro agent_send
-
# @!macro send_bang_return_and_raise
-
1
def send_off!(*args, &action)
-
raise Error.new unless send_off(*args, &action)
-
true
-
end
-
-
# @!macro agent_send
-
# @!macro send_return
-
# @param [Concurrent::ExecutorService] executor the executor on which the
-
# action is to be dispatched
-
1
def send_via(executor, *args, &action)
-
enqueue_action_job(action, args, executor)
-
end
-
-
# @!macro agent_send
-
# @!macro send_bang_return_and_raise
-
# @param [Concurrent::ExecutorService] executor the executor on which the
-
# action is to be dispatched
-
1
def send_via!(executor, *args, &action)
-
raise Error.new unless send_via(executor, *args, &action)
-
true
-
end
-
-
# Dispatches an action to the Agent and returns immediately. Subsequently,
-
# in a thread from a thread pool, the {#value} will be set to the return
-
# value of the action. Appropriate for actions that may block on IO.
-
#
-
# @param [Proc] action the action dispatch to be enqueued
-
# @return [Concurrent::Agent] self
-
# @see #send_off
-
1
def <<(action)
-
send_off(&action)
-
self
-
end
-
-
# Blocks the current thread (indefinitely!) until all actions dispatched
-
# thus far, from this thread or nested by the Agent, have occurred. Will
-
# block when {#failed?}. Will never return if a failed Agent is {#restart}
-
# with `:clear_actions` true.
-
#
-
# Returns a reference to `self` to support method chaining:
-
#
-
# ```
-
# current_value = agent.await.value
-
# ```
-
#
-
# @return [Boolean] self
-
#
-
# @!macro agent_await_warning
-
1
def await
-
wait(nil)
-
self
-
end
-
-
# Blocks the current thread until all actions dispatched thus far, from this
-
# thread or nested by the Agent, have occurred, or the timeout (in seconds)
-
# has elapsed.
-
#
-
# @param [Float] timeout the maximum number of seconds to wait
-
# @return [Boolean] true if all actions complete before timeout else false
-
#
-
# @!macro agent_await_warning
-
1
def await_for(timeout)
-
wait(timeout.to_f)
-
end
-
-
# Blocks the current thread until all actions dispatched thus far, from this
-
# thread or nested by the Agent, have occurred, or the timeout (in seconds)
-
# has elapsed.
-
#
-
# @param [Float] timeout the maximum number of seconds to wait
-
# @return [Boolean] true if all actions complete before timeout
-
#
-
# @raise [Concurrent::TimeoutError] when timout is reached
-
#
-
# @!macro agent_await_warning
-
1
def await_for!(timeout)
-
raise Concurrent::TimeoutError unless wait(timeout.to_f)
-
true
-
end
-
-
# Blocks the current thread until all actions dispatched thus far, from this
-
# thread or nested by the Agent, have occurred, or the timeout (in seconds)
-
# has elapsed. Will block indefinitely when timeout is nil or not given.
-
#
-
# Provided mainly for consistency with other classes in this library. Prefer
-
# the various `await` methods instead.
-
#
-
# @param [Float] timeout the maximum number of seconds to wait
-
# @return [Boolean] true if all actions complete before timeout else false
-
#
-
# @!macro agent_await_warning
-
1
def wait(timeout = nil)
-
latch = Concurrent::CountDownLatch.new(1)
-
enqueue_await_job(latch)
-
latch.wait(timeout)
-
end
-
-
# Is the Agent in a failed state?
-
#
-
# @see #restart
-
1
def failed?
-
!@error.value.nil?
-
end
-
-
1
alias_method :stopped?, :failed?
-
-
# When an Agent is {#failed?}, changes the Agent {#value} to `new_value`
-
# then un-fails the Agent so that action dispatches are allowed again. If
-
# the `:clear_actions` option is give and true, any actions queued on the
-
# Agent that were being held while it was failed will be discarded,
-
# otherwise those held actions will proceed. The `new_value` must pass the
-
# validator if any, or `restart` will raise an exception and the Agent will
-
# remain failed with its old {#value} and {#error}. Observers, if any, will
-
# not be notified of the new state.
-
#
-
# @param [Object] new_value the new value for the Agent once restarted
-
# @param [Hash] opts the configuration options
-
# @option opts [Symbol] :clear_actions true if all enqueued but unprocessed
-
# actions should be discarded on restart, else false (default: false)
-
# @return [Boolean] true
-
#
-
# @raise [Concurrent:AgentError] when not failed
-
1
def restart(new_value, opts = {})
-
clear_actions = opts.fetch(:clear_actions, false)
-
synchronize do
-
raise Error.new('agent is not failed') unless failed?
-
raise ValidationError unless ns_validate(new_value)
-
@current.value = new_value
-
@error.value = nil
-
@queue.clear if clear_actions
-
ns_post_next_job unless @queue.empty?
-
end
-
true
-
end
-
-
1
class << self
-
-
# Blocks the current thread (indefinitely!) until all actions dispatched
-
# thus far to all the given Agents, from this thread or nested by the
-
# given Agents, have occurred. Will block when any of the agents are
-
# failed. Will never return if a failed Agent is restart with
-
# `:clear_actions` true.
-
#
-
# @param [Array<Concurrent::Agent>] agents the Agents on which to wait
-
# @return [Boolean] true
-
#
-
# @!macro agent_await_warning
-
1
def await(*agents)
-
agents.each { |agent| agent.await }
-
true
-
end
-
-
# Blocks the current thread until all actions dispatched thus far to all
-
# the given Agents, from this thread or nested by the given Agents, have
-
# occurred, or the timeout (in seconds) has elapsed.
-
#
-
# @param [Float] timeout the maximum number of seconds to wait
-
# @param [Array<Concurrent::Agent>] agents the Agents on which to wait
-
# @return [Boolean] true if all actions complete before timeout else false
-
#
-
# @!macro agent_await_warning
-
1
def await_for(timeout, *agents)
-
end_at = Concurrent.monotonic_time + timeout.to_f
-
ok = agents.length.times do |i|
-
break false if (delay = end_at - Concurrent.monotonic_time) < 0
-
break false unless agents[i].await_for(delay)
-
end
-
!!ok
-
end
-
-
# Blocks the current thread until all actions dispatched thus far to all
-
# the given Agents, from this thread or nested by the given Agents, have
-
# occurred, or the timeout (in seconds) has elapsed.
-
#
-
# @param [Float] timeout the maximum number of seconds to wait
-
# @param [Array<Concurrent::Agent>] agents the Agents on which to wait
-
# @return [Boolean] true if all actions complete before timeout
-
#
-
# @raise [Concurrent::TimeoutError] when timout is reached
-
# @!macro agent_await_warning
-
1
def await_for!(timeout, *agents)
-
raise Concurrent::TimeoutError unless await_for(timeout, *agents)
-
true
-
end
-
end
-
-
1
private
-
-
1
def ns_initialize(initial, opts)
-
@error_mode = opts[:error_mode]
-
@error_handler = opts[:error_handler]
-
-
if @error_mode && !ERROR_MODES.include?(@error_mode)
-
raise ArgumentError.new('unrecognized error mode')
-
elsif @error_mode.nil?
-
@error_mode = @error_handler ? :continue : :fail
-
end
-
-
@error_handler ||= DEFAULT_ERROR_HANDLER
-
@validator = opts.fetch(:validator, DEFAULT_VALIDATOR)
-
@current = Concurrent::AtomicReference.new(initial)
-
@error = Concurrent::AtomicReference.new(nil)
-
@caller = Concurrent::ThreadLocalVar.new(nil)
-
@queue = []
-
-
self.observers = Collection::CopyOnNotifyObserverSet.new
-
end
-
-
1
def enqueue_action_job(action, args, executor)
-
raise ArgumentError.new('no action given') unless action
-
job = Job.new(action, args, executor, @caller.value || Thread.current.object_id)
-
synchronize { ns_enqueue_job(job) }
-
end
-
-
1
def enqueue_await_job(latch)
-
synchronize do
-
if (index = ns_find_last_job_for_thread)
-
job = Job.new(AWAIT_ACTION, [latch], Concurrent.global_immediate_executor,
-
Thread.current.object_id)
-
ns_enqueue_job(job, index+1)
-
else
-
latch.count_down
-
true
-
end
-
end
-
end
-
-
1
def ns_enqueue_job(job, index = nil)
-
# a non-nil index means this is an await job
-
return false if index.nil? && failed?
-
index ||= @queue.length
-
@queue.insert(index, job)
-
# if this is the only job, post to executor
-
ns_post_next_job if @queue.length == 1
-
true
-
end
-
-
1
def ns_post_next_job
-
@queue.first.executor.post { execute_next_job }
-
end
-
-
1
def execute_next_job
-
job = synchronize { @queue.first }
-
old_value = @current.value
-
-
@caller.value = job.caller # for nested actions
-
new_value = job.action.call(old_value, *job.args)
-
@caller.value = nil
-
-
return if new_value == AWAIT_FLAG
-
-
if ns_validate(new_value)
-
@current.value = new_value
-
observers.notify_observers(Time.now, old_value, new_value)
-
else
-
handle_error(ValidationError.new)
-
end
-
rescue => error
-
handle_error(error)
-
ensure
-
synchronize do
-
@queue.shift
-
unless failed? || @queue.empty?
-
ns_post_next_job
-
end
-
end
-
end
-
-
1
def ns_validate(value)
-
@validator.call(value)
-
rescue
-
false
-
end
-
-
1
def handle_error(error)
-
# stop new jobs from posting
-
@error.value = error if @error_mode == :fail
-
@error_handler.call(self, error)
-
rescue
-
# do nothing
-
end
-
-
1
def ns_find_last_job_for_thread
-
@queue.rindex { |job| job.caller == Thread.current.object_id }
-
end
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/thread_safe/util'
-
-
1
module Concurrent
-
-
# @!macro concurrent_array
-
#
-
# A thread-safe subclass of Array. This version locks against the object
-
# itself for every method call, ensuring only one thread can be reading
-
# or writing at a time. This includes iteration methods like `#each`.
-
#
-
# @note `a += b` is **not** a **thread-safe** operation on
-
# `Concurrent::Array`. It reads array `a`, then it creates new `Concurrent::Array`
-
# which is concatenation of `a` and `b`, then it writes the concatenation to `a`.
-
# The read and write are independent operations they do not form a single atomic
-
# operation therefore when two `+=` operations are executed concurrently updates
-
# may be lost. Use `#concat` instead.
-
#
-
# @see http://ruby-doc.org/core-2.2.0/Array.html Ruby standard library `Array`
-
-
# @!macro internal_implementation_note
-
ArrayImplementation = case
-
1
when Concurrent.on_cruby?
-
# Array is thread-safe in practice because CRuby runs
-
# threads one at a time and does not do context
-
# switching during the execution of C functions.
-
1
::Array
-
-
when Concurrent.on_jruby?
-
require 'jruby/synchronized'
-
-
class JRubyArray < ::Array
-
include JRuby::Synchronized
-
end
-
JRubyArray
-
-
when Concurrent.on_rbx?
-
require 'monitor'
-
require 'concurrent/thread_safe/util/data_structures'
-
-
class RbxArray < ::Array
-
end
-
-
ThreadSafe::Util.make_synchronized_on_rbx RbxArray
-
RbxArray
-
-
when Concurrent.on_truffleruby?
-
require 'concurrent/thread_safe/util/data_structures'
-
-
class TruffleRubyArray < ::Array
-
end
-
-
ThreadSafe::Util.make_synchronized_on_truffleruby TruffleRubyArray
-
TruffleRubyArray
-
-
else
-
warn 'Possibly unsupported Ruby implementation'
-
::Array
-
end
-
1
private_constant :ArrayImplementation
-
-
# @!macro concurrent_array
-
1
class Array < ArrayImplementation
-
end
-
-
end
-
1
require 'concurrent/configuration'
-
1
require 'concurrent/ivar'
-
1
require 'concurrent/synchronization/lockable_object'
-
-
1
module Concurrent
-
-
# A mixin module that provides simple asynchronous behavior to a class,
-
# turning it into a simple actor. Loosely based on Erlang's
-
# [gen_server](http://www.erlang.org/doc/man/gen_server.html), but without
-
# supervision or linking.
-
#
-
# A more feature-rich {Concurrent::Actor} is also available when the
-
# capabilities of `Async` are too limited.
-
#
-
# ```cucumber
-
# Feature:
-
# As a stateful, plain old Ruby class
-
# I want safe, asynchronous behavior
-
# So my long-running methods don't block the main thread
-
# ```
-
#
-
# The `Async` module is a way to mix simple yet powerful asynchronous
-
# capabilities into any plain old Ruby object or class, turning each object
-
# into a simple Actor. Method calls are processed on a background thread. The
-
# caller is free to perform other actions while processing occurs in the
-
# background.
-
#
-
# Method calls to the asynchronous object are made via two proxy methods:
-
# `async` (alias `cast`) and `await` (alias `call`). These proxy methods post
-
# the method call to the object's background thread and return a "future"
-
# which will eventually contain the result of the method call.
-
#
-
# This behavior is loosely patterned after Erlang's `gen_server` behavior.
-
# When an Erlang module implements the `gen_server` behavior it becomes
-
# inherently asynchronous. The `start` or `start_link` function spawns a
-
# process (similar to a thread but much more lightweight and efficient) and
-
# returns the ID of the process. Using the process ID, other processes can
-
# send messages to the `gen_server` via the `cast` and `call` methods. Unlike
-
# Erlang's `gen_server`, however, `Async` classes do not support linking or
-
# supervision trees.
-
#
-
# ## Basic Usage
-
#
-
# When this module is mixed into a class, objects of the class become inherently
-
# asynchronous. Each object gets its own background thread on which to post
-
# asynchronous method calls. Asynchronous method calls are executed in the
-
# background one at a time in the order they are received.
-
#
-
# To create an asynchronous class, simply mix in the `Concurrent::Async` module:
-
#
-
# ```
-
# class Hello
-
# include Concurrent::Async
-
#
-
# def hello(name)
-
# "Hello, #{name}!"
-
# end
-
# end
-
# ```
-
#
-
# When defining a constructor it is critical that the first line be a call to
-
# `super` with no arguments. The `super` method initializes the background
-
# thread and other asynchronous components.
-
#
-
# ```
-
# class BackgroundLogger
-
# include Concurrent::Async
-
#
-
# def initialize(level)
-
# super()
-
# @logger = Logger.new(STDOUT)
-
# @logger.level = level
-
# end
-
#
-
# def info(msg)
-
# @logger.info(msg)
-
# end
-
# end
-
# ```
-
#
-
# Mixing this module into a class provides each object two proxy methods:
-
# `async` and `await`. These methods are thread safe with respect to the
-
# enclosing object. The former proxy allows methods to be called
-
# asynchronously by posting to the object's internal thread. The latter proxy
-
# allows a method to be called synchronously but does so safely with respect
-
# to any pending asynchronous method calls and ensures proper ordering. Both
-
# methods return a {Concurrent::IVar} which can be inspected for the result
-
# of the proxied method call. Calling a method with `async` will return a
-
# `:pending` `IVar` whereas `await` will return a `:complete` `IVar`.
-
#
-
# ```
-
# class Echo
-
# include Concurrent::Async
-
#
-
# def echo(msg)
-
# print "#{msg}\n"
-
# end
-
# end
-
#
-
# horn = Echo.new
-
# horn.echo('zero') # synchronous, not thread-safe
-
# # returns the actual return value of the method
-
#
-
# horn.async.echo('one') # asynchronous, non-blocking, thread-safe
-
# # returns an IVar in the :pending state
-
#
-
# horn.await.echo('two') # synchronous, blocking, thread-safe
-
# # returns an IVar in the :complete state
-
# ```
-
#
-
# ## Let It Fail
-
#
-
# The `async` and `await` proxy methods have built-in error protection based
-
# on Erlang's famous "let it fail" philosophy. Instance methods should not be
-
# programmed defensively. When an exception is raised by a delegated method
-
# the proxy will rescue the exception, expose it to the caller as the `reason`
-
# attribute of the returned future, then process the next method call.
-
#
-
# ## Calling Methods Internally
-
#
-
# External method calls should *always* use the `async` and `await` proxy
-
# methods. When one method calls another method, the `async` proxy should
-
# rarely be used and the `await` proxy should *never* be used.
-
#
-
# When an object calls one of its own methods using the `await` proxy the
-
# second call will be enqueued *behind* the currently running method call.
-
# Any attempt to wait on the result will fail as the second call will never
-
# run until after the current call completes.
-
#
-
# Calling a method using the `await` proxy from within a method that was
-
# itself called using `async` or `await` will irreversibly deadlock the
-
# object. Do *not* do this, ever.
-
#
-
# ## Instance Variables and Attribute Accessors
-
#
-
# Instance variables do not need to be thread-safe so long as they are private.
-
# Asynchronous method calls are processed in the order they are received and
-
# are processed one at a time. Therefore private instance variables can only
-
# be accessed by one thread at a time. This is inherently thread-safe.
-
#
-
# When using private instance variables within asynchronous methods, the best
-
# practice is to read the instance variable into a local variable at the start
-
# of the method then update the instance variable at the *end* of the method.
-
# This way, should an exception be raised during method execution the internal
-
# state of the object will not have been changed.
-
#
-
# ### Reader Attributes
-
#
-
# The use of `attr_reader` is discouraged. Internal state exposed externally,
-
# when necessary, should be done through accessor methods. The instance
-
# variables exposed by these methods *must* be thread-safe, or they must be
-
# called using the `async` and `await` proxy methods. These two approaches are
-
# subtly different.
-
#
-
# When internal state is accessed via the `async` and `await` proxy methods,
-
# the returned value represents the object's state *at the time the call is
-
# processed*, which may *not* be the state of the object at the time the call
-
# is made.
-
#
-
# To get the state *at the current* time, irrespective of an enqueued method
-
# calls, a reader method must be called directly. This is inherently unsafe
-
# unless the instance variable is itself thread-safe, preferably using one
-
# of the thread-safe classes within this library. Because the thread-safe
-
# classes within this library are internally-locking or non-locking, they can
-
# be safely used from within asynchronous methods without causing deadlocks.
-
#
-
# Generally speaking, the best practice is to *not* expose internal state via
-
# reader methods. The best practice is to simply use the method's return value.
-
#
-
# ### Writer Attributes
-
#
-
# Writer attributes should never be used with asynchronous classes. Changing
-
# the state externally, even when done in the thread-safe way, is not logically
-
# consistent. Changes to state need to be timed with respect to all asynchronous
-
# method calls which my be in-process or enqueued. The only safe practice is to
-
# pass all necessary data to each method as arguments and let the method update
-
# the internal state as necessary.
-
#
-
# ## Class Constants, Variables, and Methods
-
#
-
# ### Class Constants
-
#
-
# Class constants do not need to be thread-safe. Since they are read-only and
-
# immutable they may be safely read both externally and from within
-
# asynchronous methods.
-
#
-
# ### Class Variables
-
#
-
# Class variables should be avoided. Class variables represent shared state.
-
# Shared state is anathema to concurrency. Should there be a need to share
-
# state using class variables they *must* be thread-safe, preferably
-
# using the thread-safe classes within this library. When updating class
-
# variables, never assign a new value/object to the variable itself. Assignment
-
# is not thread-safe in Ruby. Instead, use the thread-safe update functions
-
# of the variable itself to change the value.
-
#
-
# The best practice is to *never* use class variables with `Async` classes.
-
#
-
# ### Class Methods
-
#
-
# Class methods which are pure functions are safe. Class methods which modify
-
# class variables should be avoided, for all the reasons listed above.
-
#
-
# ## An Important Note About Thread Safe Guarantees
-
#
-
# > Thread safe guarantees can only be made when asynchronous method calls
-
# > are not mixed with direct method calls. Use only direct method calls
-
# > when the object is used exclusively on a single thread. Use only
-
# > `async` and `await` when the object is shared between threads. Once you
-
# > call a method using `async` or `await`, you should no longer call methods
-
# > directly on the object. Use `async` and `await` exclusively from then on.
-
#
-
# @example
-
#
-
# class Echo
-
# include Concurrent::Async
-
#
-
# def echo(msg)
-
# print "#{msg}\n"
-
# end
-
# end
-
#
-
# horn = Echo.new
-
# horn.echo('zero') # synchronous, not thread-safe
-
# # returns the actual return value of the method
-
#
-
# horn.async.echo('one') # asynchronous, non-blocking, thread-safe
-
# # returns an IVar in the :pending state
-
#
-
# horn.await.echo('two') # synchronous, blocking, thread-safe
-
# # returns an IVar in the :complete state
-
#
-
# @see Concurrent::Actor
-
# @see https://en.wikipedia.org/wiki/Actor_model "Actor Model" at Wikipedia
-
# @see http://www.erlang.org/doc/man/gen_server.html Erlang gen_server
-
# @see http://c2.com/cgi/wiki?LetItCrash "Let It Crash" at http://c2.com/
-
1
module Async
-
-
# @!method self.new(*args, &block)
-
#
-
# Instanciate a new object and ensure proper initialization of the
-
# synchronization mechanisms.
-
#
-
# @param [Array<Object>] args Zero or more arguments to be passed to the
-
# object's initializer.
-
# @param [Proc] block Optional block to pass to the object's initializer.
-
# @return [Object] A properly initialized object of the asynchronous class.
-
-
# Check for the presence of a method on an object and determine if a given
-
# set of arguments matches the required arity.
-
#
-
# @param [Object] obj the object to check against
-
# @param [Symbol] method the method to check the object for
-
# @param [Array] args zero or more arguments for the arity check
-
#
-
# @raise [NameError] the object does not respond to `method` method
-
# @raise [ArgumentError] the given `args` do not match the arity of `method`
-
#
-
# @note This check is imperfect because of the way Ruby reports the arity of
-
# methods with a variable number of arguments. It is possible to determine
-
# if too few arguments are given but impossible to determine if too many
-
# arguments are given. This check may also fail to recognize dynamic behavior
-
# of the object, such as methods simulated with `method_missing`.
-
#
-
# @see http://www.ruby-doc.org/core-2.1.1/Method.html#method-i-arity Method#arity
-
# @see http://ruby-doc.org/core-2.1.0/Object.html#method-i-respond_to-3F Object#respond_to?
-
# @see http://www.ruby-doc.org/core-2.1.0/BasicObject.html#method-i-method_missing BasicObject#method_missing
-
#
-
# @!visibility private
-
1
def self.validate_argc(obj, method, *args)
-
argc = args.length
-
arity = obj.method(method).arity
-
-
if arity >= 0 && argc != arity
-
raise ArgumentError.new("wrong number of arguments (#{argc} for #{arity})")
-
elsif arity < 0 && (arity = (arity + 1).abs) > argc
-
raise ArgumentError.new("wrong number of arguments (#{argc} for #{arity}..*)")
-
end
-
end
-
-
# @!visibility private
-
1
def self.included(base)
-
base.singleton_class.send(:alias_method, :original_new, :new)
-
base.extend(ClassMethods)
-
super(base)
-
end
-
-
# @!visibility private
-
1
module ClassMethods
-
1
def new(*args, &block)
-
obj = original_new(*args, &block)
-
obj.send(:init_synchronization)
-
obj
-
end
-
end
-
1
private_constant :ClassMethods
-
-
# Delegates asynchronous, thread-safe method calls to the wrapped object.
-
#
-
# @!visibility private
-
1
class AsyncDelegator < Synchronization::LockableObject
-
1
safe_initialization!
-
-
# Create a new delegator object wrapping the given delegate.
-
#
-
# @param [Object] delegate the object to wrap and delegate method calls to
-
1
def initialize(delegate)
-
super()
-
@delegate = delegate
-
@queue = []
-
@executor = Concurrent.global_io_executor
-
end
-
-
# Delegates method calls to the wrapped object.
-
#
-
# @param [Symbol] method the method being called
-
# @param [Array] args zero or more arguments to the method
-
#
-
# @return [IVar] the result of the method call
-
#
-
# @raise [NameError] the object does not respond to `method` method
-
# @raise [ArgumentError] the given `args` do not match the arity of `method`
-
1
def method_missing(method, *args, &block)
-
super unless @delegate.respond_to?(method)
-
Async::validate_argc(@delegate, method, *args)
-
-
ivar = Concurrent::IVar.new
-
synchronize do
-
@queue.push [ivar, method, args, block]
-
@executor.post { perform } if @queue.length == 1
-
end
-
-
ivar
-
end
-
-
# Check whether the method is responsive
-
#
-
# @param [Symbol] method the method being called
-
1
def respond_to_missing?(method, include_private = false)
-
@delegate.respond_to?(method) || super
-
end
-
-
# Perform all enqueued tasks.
-
#
-
# This method must be called from within the executor. It must not be
-
# called while already running. It will loop until the queue is empty.
-
1
def perform
-
loop do
-
ivar, method, args, block = synchronize { @queue.first }
-
break unless ivar # queue is empty
-
-
begin
-
ivar.set(@delegate.send(method, *args, &block))
-
rescue => error
-
ivar.fail(error)
-
end
-
-
synchronize do
-
@queue.shift
-
return if @queue.empty?
-
end
-
end
-
end
-
end
-
1
private_constant :AsyncDelegator
-
-
# Delegates synchronous, thread-safe method calls to the wrapped object.
-
#
-
# @!visibility private
-
1
class AwaitDelegator
-
-
# Create a new delegator object wrapping the given delegate.
-
#
-
# @param [AsyncDelegator] delegate the object to wrap and delegate method calls to
-
1
def initialize(delegate)
-
@delegate = delegate
-
end
-
-
# Delegates method calls to the wrapped object.
-
#
-
# @param [Symbol] method the method being called
-
# @param [Array] args zero or more arguments to the method
-
#
-
# @return [IVar] the result of the method call
-
#
-
# @raise [NameError] the object does not respond to `method` method
-
# @raise [ArgumentError] the given `args` do not match the arity of `method`
-
1
def method_missing(method, *args, &block)
-
ivar = @delegate.send(method, *args, &block)
-
ivar.wait
-
ivar
-
end
-
-
# Check whether the method is responsive
-
#
-
# @param [Symbol] method the method being called
-
1
def respond_to_missing?(method, include_private = false)
-
@delegate.respond_to?(method) || super
-
end
-
end
-
1
private_constant :AwaitDelegator
-
-
# Causes the chained method call to be performed asynchronously on the
-
# object's thread. The delegated method will return a future in the
-
# `:pending` state and the method call will have been scheduled on the
-
# object's thread. The final disposition of the method call can be obtained
-
# by inspecting the returned future.
-
#
-
# @!macro async_thread_safety_warning
-
# @note The method call is guaranteed to be thread safe with respect to
-
# all other method calls against the same object that are called with
-
# either `async` or `await`. The mutable nature of Ruby references
-
# (and object orientation in general) prevent any other thread safety
-
# guarantees. Do NOT mix direct method calls with delegated method calls.
-
# Use *only* delegated method calls when sharing the object between threads.
-
#
-
# @return [Concurrent::IVar] the pending result of the asynchronous operation
-
#
-
# @raise [NameError] the object does not respond to the requested method
-
# @raise [ArgumentError] the given `args` do not match the arity of
-
# the requested method
-
1
def async
-
@__async_delegator__
-
end
-
1
alias_method :cast, :async
-
-
# Causes the chained method call to be performed synchronously on the
-
# current thread. The delegated will return a future in either the
-
# `:fulfilled` or `:rejected` state and the delegated method will have
-
# completed. The final disposition of the delegated method can be obtained
-
# by inspecting the returned future.
-
#
-
# @!macro async_thread_safety_warning
-
#
-
# @return [Concurrent::IVar] the completed result of the synchronous operation
-
#
-
# @raise [NameError] the object does not respond to the requested method
-
# @raise [ArgumentError] the given `args` do not match the arity of the
-
# requested method
-
1
def await
-
@__await_delegator__
-
end
-
1
alias_method :call, :await
-
-
# Initialize the internal serializer and other stnchronization mechanisms.
-
#
-
# @note This method *must* be called immediately upon object construction.
-
# This is the only way thread-safe initialization can be guaranteed.
-
#
-
# @!visibility private
-
1
def init_synchronization
-
return self if defined?(@__async_initialized__) && @__async_initialized__
-
@__async_initialized__ = true
-
@__async_delegator__ = AsyncDelegator.new(self)
-
@__await_delegator__ = AwaitDelegator.new(@__async_delegator__)
-
self
-
end
-
end
-
end
-
1
require 'concurrent/atomic/atomic_reference'
-
1
require 'concurrent/collection/copy_on_notify_observer_set'
-
1
require 'concurrent/concern/observable'
-
1
require 'concurrent/synchronization'
-
-
# @!macro thread_safe_variable_comparison
-
#
-
# ## Thread-safe Variable Classes
-
#
-
# Each of the thread-safe variable classes is designed to solve a different
-
# problem. In general:
-
#
-
# * *{Concurrent::Agent}:* Shared, mutable variable providing independent,
-
# uncoordinated, *asynchronous* change of individual values. Best used when
-
# the value will undergo frequent, complex updates. Suitable when the result
-
# of an update does not need to be known immediately.
-
# * *{Concurrent::Atom}:* Shared, mutable variable providing independent,
-
# uncoordinated, *synchronous* change of individual values. Best used when
-
# the value will undergo frequent reads but only occasional, though complex,
-
# updates. Suitable when the result of an update must be known immediately.
-
# * *{Concurrent::AtomicReference}:* A simple object reference that can be updated
-
# atomically. Updates are synchronous but fast. Best used when updates a
-
# simple set operations. Not suitable when updates are complex.
-
# {Concurrent::AtomicBoolean} and {Concurrent::AtomicFixnum} are similar
-
# but optimized for the given data type.
-
# * *{Concurrent::Exchanger}:* Shared, stateless synchronization point. Used
-
# when two or more threads need to exchange data. The threads will pair then
-
# block on each other until the exchange is complete.
-
# * *{Concurrent::MVar}:* Shared synchronization point. Used when one thread
-
# must give a value to another, which must take the value. The threads will
-
# block on each other until the exchange is complete.
-
# * *{Concurrent::ThreadLocalVar}:* Shared, mutable, isolated variable which
-
# holds a different value for each thread which has access. Often used as
-
# an instance variable in objects which must maintain different state
-
# for different threads.
-
# * *{Concurrent::TVar}:* Shared, mutable variables which provide
-
# *coordinated*, *synchronous*, change of *many* stated. Used when multiple
-
# value must change together, in an all-or-nothing transaction.
-
-
-
1
module Concurrent
-
-
# Atoms provide a way to manage shared, synchronous, independent state.
-
#
-
# An atom is initialized with an initial value and an optional validation
-
# proc. At any time the value of the atom can be synchronously and safely
-
# changed. If a validator is given at construction then any new value
-
# will be checked against the validator and will be rejected if the
-
# validator returns false or raises an exception.
-
#
-
# There are two ways to change the value of an atom: {#compare_and_set} and
-
# {#swap}. The former will set the new value if and only if it validates and
-
# the current value matches the new value. The latter will atomically set the
-
# new value to the result of running the given block if and only if that
-
# value validates.
-
#
-
# ## Example
-
#
-
# ```
-
# def next_fibonacci(set = nil)
-
# return [0, 1] if set.nil?
-
# set + [set[-2..-1].reduce{|sum,x| sum + x }]
-
# end
-
#
-
# # create an atom with an initial value
-
# atom = Concurrent::Atom.new(next_fibonacci)
-
#
-
# # send a few update requests
-
# 5.times do
-
# atom.swap{|set| next_fibonacci(set) }
-
# end
-
#
-
# # get the current value
-
# atom.value #=> [0, 1, 1, 2, 3, 5, 8]
-
# ```
-
#
-
# ## Observation
-
#
-
# Atoms support observers through the {Concurrent::Observable} mixin module.
-
# Notification of observers occurs every time the value of the Atom changes.
-
# When notified the observer will receive three arguments: `time`, `old_value`,
-
# and `new_value`. The `time` argument is the time at which the value change
-
# occurred. The `old_value` is the value of the Atom when the change began
-
# The `new_value` is the value to which the Atom was set when the change
-
# completed. Note that `old_value` and `new_value` may be the same. This is
-
# not an error. It simply means that the change operation returned the same
-
# value.
-
#
-
# Unlike in Clojure, `Atom` cannot participate in {Concurrent::TVar} transactions.
-
#
-
# @!macro thread_safe_variable_comparison
-
#
-
# @see http://clojure.org/atoms Clojure Atoms
-
# @see http://clojure.org/state Values and Change - Clojure's approach to Identity and State
-
1
class Atom < Synchronization::Object
-
1
include Concern::Observable
-
-
1
safe_initialization!
-
1
attr_atomic(:value)
-
1
private :value=, :swap_value, :compare_and_set_value, :update_value
-
1
public :value
-
1
alias_method :deref, :value
-
-
# @!method value
-
# The current value of the atom.
-
#
-
# @return [Object] The current value.
-
-
# Create a new atom with the given initial value.
-
#
-
# @param [Object] value The initial value
-
# @param [Hash] opts The options used to configure the atom
-
# @option opts [Proc] :validator (nil) Optional proc used to validate new
-
# values. It must accept one and only one argument which will be the
-
# intended new value. The validator will return true if the new value
-
# is acceptable else return false (preferrably) or raise an exception.
-
#
-
# @!macro deref_options
-
#
-
# @raise [ArgumentError] if the validator is not a `Proc` (when given)
-
1
def initialize(value, opts = {})
-
super()
-
@Validator = opts.fetch(:validator, -> v { true })
-
self.observers = Collection::CopyOnNotifyObserverSet.new
-
self.value = value
-
end
-
-
# Atomically swaps the value of atom using the given block. The current
-
# value will be passed to the block, as will any arguments passed as
-
# arguments to the function. The new value will be validated against the
-
# (optional) validator proc given at construction. If validation fails the
-
# value will not be changed.
-
#
-
# Internally, {#swap} reads the current value, applies the block to it, and
-
# attempts to compare-and-set it in. Since another thread may have changed
-
# the value in the intervening time, it may have to retry, and does so in a
-
# spin loop. The net effect is that the value will always be the result of
-
# the application of the supplied block to a current value, atomically.
-
# However, because the block might be called multiple times, it must be free
-
# of side effects.
-
#
-
# @note The given block may be called multiple times, and thus should be free
-
# of side effects.
-
#
-
# @param [Object] args Zero or more arguments passed to the block.
-
#
-
# @yield [value, args] Calculates a new value for the atom based on the
-
# current value and any supplied arguments.
-
# @yieldparam value [Object] The current value of the atom.
-
# @yieldparam args [Object] All arguments passed to the function, in order.
-
# @yieldreturn [Object] The intended new value of the atom.
-
#
-
# @return [Object] The final value of the atom after all operations and
-
# validations are complete.
-
#
-
# @raise [ArgumentError] When no block is given.
-
1
def swap(*args)
-
raise ArgumentError.new('no block given') unless block_given?
-
-
loop do
-
old_value = value
-
new_value = yield(old_value, *args)
-
begin
-
break old_value unless valid?(new_value)
-
break new_value if compare_and_set(old_value, new_value)
-
rescue
-
break old_value
-
end
-
end
-
end
-
-
# Atomically sets the value of atom to the new value if and only if the
-
# current value of the atom is identical to the old value and the new
-
# value successfully validates against the (optional) validator given
-
# at construction.
-
#
-
# @param [Object] old_value The expected current value.
-
# @param [Object] new_value The intended new value.
-
#
-
# @return [Boolean] True if the value is changed else false.
-
1
def compare_and_set(old_value, new_value)
-
if valid?(new_value) && compare_and_set_value(old_value, new_value)
-
observers.notify_observers(Time.now, old_value, new_value)
-
true
-
else
-
false
-
end
-
end
-
-
# Atomically sets the value of atom to the new value without regard for the
-
# current value so long as the new value successfully validates against the
-
# (optional) validator given at construction.
-
#
-
# @param [Object] new_value The intended new value.
-
#
-
# @return [Object] The final value of the atom after all operations and
-
# validations are complete.
-
1
def reset(new_value)
-
old_value = value
-
if valid?(new_value)
-
self.value = new_value
-
observers.notify_observers(Time.now, old_value, new_value)
-
new_value
-
else
-
old_value
-
end
-
end
-
-
1
private
-
-
# Is the new value valid?
-
#
-
# @param [Object] new_value The intended new value.
-
# @return [Boolean] false if the validator function returns false or raises
-
# an exception else true
-
1
def valid?(new_value)
-
@Validator.call(new_value)
-
rescue
-
false
-
end
-
end
-
end
-
1
require 'concurrent/constants'
-
-
1
module Concurrent
-
-
# @!macro thread_local_var
-
# @!macro internal_implementation_note
-
# @!visibility private
-
1
class AbstractThreadLocalVar
-
-
# @!macro thread_local_var_method_initialize
-
1
def initialize(default = nil, &default_block)
-
if default && block_given?
-
raise ArgumentError, "Cannot use both value and block as default value"
-
end
-
-
if block_given?
-
@default_block = default_block
-
@default = nil
-
else
-
@default_block = nil
-
@default = default
-
end
-
-
allocate_storage
-
end
-
-
# @!macro thread_local_var_method_get
-
1
def value
-
raise NotImplementedError
-
end
-
-
# @!macro thread_local_var_method_set
-
1
def value=(value)
-
raise NotImplementedError
-
end
-
-
# @!macro thread_local_var_method_bind
-
1
def bind(value, &block)
-
if block_given?
-
old_value = self.value
-
begin
-
self.value = value
-
yield
-
ensure
-
self.value = old_value
-
end
-
end
-
end
-
-
1
protected
-
-
# @!visibility private
-
1
def allocate_storage
-
raise NotImplementedError
-
end
-
-
# @!visibility private
-
1
def default
-
if @default_block
-
self.value = @default_block.call
-
else
-
@default
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
# An atomic reference which maintains an object reference along with a mark bit
-
# that can be updated atomically.
-
#
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/AtomicMarkableReference.html
-
# java.util.concurrent.atomic.AtomicMarkableReference
-
1
class AtomicMarkableReference < ::Concurrent::Synchronization::Object
-
-
1
attr_atomic(:reference)
-
1
private :reference, :reference=, :swap_reference, :compare_and_set_reference, :update_reference
-
-
1
def initialize(value = nil, mark = false)
-
super()
-
self.reference = immutable_array(value, mark)
-
end
-
-
# Atomically sets the value and mark to the given updated value and
-
# mark given both:
-
# - the current value == the expected value &&
-
# - the current mark == the expected mark
-
#
-
# @param [Object] expected_val the expected value
-
# @param [Object] new_val the new value
-
# @param [Boolean] expected_mark the expected mark
-
# @param [Boolean] new_mark the new mark
-
#
-
# @return [Boolean] `true` if successful. A `false` return indicates
-
# that the actual value was not equal to the expected value or the
-
# actual mark was not equal to the expected mark
-
1
def compare_and_set(expected_val, new_val, expected_mark, new_mark)
-
# Memoize a valid reference to the current AtomicReference for
-
# later comparison.
-
current = reference
-
curr_val, curr_mark = current
-
-
# Ensure that that the expected marks match.
-
return false unless expected_mark == curr_mark
-
-
if expected_val.is_a? Numeric
-
# If the object is a numeric, we need to ensure we are comparing
-
# the numerical values
-
return false unless expected_val == curr_val
-
else
-
# Otherwise, we need to ensure we are comparing the object identity.
-
# Theoretically, this could be incorrect if a user monkey-patched
-
# `Object#equal?`, but they should know that they are playing with
-
# fire at that point.
-
return false unless expected_val.equal? curr_val
-
end
-
-
prospect = immutable_array(new_val, new_mark)
-
-
compare_and_set_reference current, prospect
-
end
-
-
1
alias_method :compare_and_swap, :compare_and_set
-
-
# Gets the current reference and marked values.
-
#
-
# @return [Array] the current reference and marked values
-
1
def get
-
reference
-
end
-
-
# Gets the current value of the reference
-
#
-
# @return [Object] the current value of the reference
-
1
def value
-
reference[0]
-
end
-
-
# Gets the current marked value
-
#
-
# @return [Boolean] the current marked value
-
1
def mark
-
reference[1]
-
end
-
-
1
alias_method :marked?, :mark
-
-
# _Unconditionally_ sets to the given value of both the reference and
-
# the mark.
-
#
-
# @param [Object] new_val the new value
-
# @param [Boolean] new_mark the new mark
-
#
-
# @return [Array] both the new value and the new mark
-
1
def set(new_val, new_mark)
-
self.reference = immutable_array(new_val, new_mark)
-
end
-
-
# Pass the current value and marked state to the given block, replacing it
-
# with the block's results. May retry if the value changes during the
-
# block's execution.
-
#
-
# @yield [Object] Calculate a new value and marked state for the atomic
-
# reference using given (old) value and (old) marked
-
# @yieldparam [Object] old_val the starting value of the atomic reference
-
# @yieldparam [Boolean] old_mark the starting state of marked
-
#
-
# @return [Array] the new value and new mark
-
1
def update
-
loop do
-
old_val, old_mark = reference
-
new_val, new_mark = yield old_val, old_mark
-
-
if compare_and_set old_val, new_val, old_mark, new_mark
-
return immutable_array(new_val, new_mark)
-
end
-
end
-
end
-
-
# Pass the current value to the given block, replacing it
-
# with the block's result. Raise an exception if the update
-
# fails.
-
#
-
# @yield [Object] Calculate a new value and marked state for the atomic
-
# reference using given (old) value and (old) marked
-
# @yieldparam [Object] old_val the starting value of the atomic reference
-
# @yieldparam [Boolean] old_mark the starting state of marked
-
#
-
# @return [Array] the new value and marked state
-
#
-
# @raise [Concurrent::ConcurrentUpdateError] if the update fails
-
1
def try_update!
-
old_val, old_mark = reference
-
new_val, new_mark = yield old_val, old_mark
-
-
unless compare_and_set old_val, new_val, old_mark, new_mark
-
fail ::Concurrent::ConcurrentUpdateError,
-
'AtomicMarkableReference: Update failed due to race condition.',
-
'Note: If you would like to guarantee an update, please use ' +
-
'the `AtomicMarkableReference#update` method.'
-
end
-
-
immutable_array(new_val, new_mark)
-
end
-
-
# Pass the current value to the given block, replacing it with the
-
# block's result. Simply return nil if update fails.
-
#
-
# @yield [Object] Calculate a new value and marked state for the atomic
-
# reference using given (old) value and (old) marked
-
# @yieldparam [Object] old_val the starting value of the atomic reference
-
# @yieldparam [Boolean] old_mark the starting state of marked
-
#
-
# @return [Array] the new value and marked state, or nil if
-
# the update failed
-
1
def try_update
-
old_val, old_mark = reference
-
new_val, new_mark = yield old_val, old_mark
-
-
return unless compare_and_set old_val, new_val, old_mark, new_mark
-
-
immutable_array(new_val, new_mark)
-
end
-
-
1
private
-
-
1
def immutable_array(*args)
-
args.freeze
-
end
-
end
-
end
-
1
require 'concurrent/synchronization'
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/atomic_reference/numeric_cas_wrapper'
-
-
# Shim for TruffleRuby::AtomicReference
-
1
if Concurrent.on_truffleruby? && !defined?(TruffleRuby::AtomicReference)
-
# @!visibility private
-
module TruffleRuby
-
AtomicReference = Truffle::AtomicReference
-
end
-
end
-
-
1
module Concurrent
-
-
# Define update methods that use direct paths
-
#
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
module AtomicDirectUpdate
-
-
# @!macro atomic_reference_method_update
-
#
-
# Pass the current value to the given block, replacing it
-
# with the block's result. May retry if the value changes
-
# during the block's execution.
-
#
-
# @yield [Object] Calculate a new value for the atomic reference using
-
# given (old) value
-
# @yieldparam [Object] old_value the starting value of the atomic reference
-
# @return [Object] the new value
-
1
def update
-
true until compare_and_set(old_value = get, new_value = yield(old_value))
-
new_value
-
end
-
-
# @!macro atomic_reference_method_try_update
-
#
-
# Pass the current value to the given block, replacing it
-
# with the block's result. Return nil if the update fails.
-
#
-
# @yield [Object] Calculate a new value for the atomic reference using
-
# given (old) value
-
# @yieldparam [Object] old_value the starting value of the atomic reference
-
# @note This method was altered to avoid raising an exception by default.
-
# Instead, this method now returns `nil` in case of failure. For more info,
-
# please see: https://github.com/ruby-concurrency/concurrent-ruby/pull/336
-
# @return [Object] the new value, or nil if update failed
-
1
def try_update
-
old_value = get
-
new_value = yield old_value
-
-
return unless compare_and_set old_value, new_value
-
-
new_value
-
end
-
-
# @!macro atomic_reference_method_try_update!
-
#
-
# Pass the current value to the given block, replacing it
-
# with the block's result. Raise an exception if the update
-
# fails.
-
#
-
# @yield [Object] Calculate a new value for the atomic reference using
-
# given (old) value
-
# @yieldparam [Object] old_value the starting value of the atomic reference
-
# @note This behavior mimics the behavior of the original
-
# `AtomicReference#try_update` API. The reason this was changed was to
-
# avoid raising exceptions (which are inherently slow) by default. For more
-
# info: https://github.com/ruby-concurrency/concurrent-ruby/pull/336
-
# @return [Object] the new value
-
# @raise [Concurrent::ConcurrentUpdateError] if the update fails
-
1
def try_update!
-
old_value = get
-
new_value = yield old_value
-
unless compare_and_set(old_value, new_value)
-
if $VERBOSE
-
raise ConcurrentUpdateError, "Update failed"
-
else
-
raise ConcurrentUpdateError, "Update failed", ConcurrentUpdateError::CONC_UP_ERR_BACKTRACE
-
end
-
end
-
new_value
-
end
-
end
-
-
1
require 'concurrent/atomic_reference/mutex_atomic'
-
-
# @!macro atomic_reference
-
#
-
# An object reference that may be updated atomically. All read and write
-
# operations have java volatile semantic.
-
#
-
# @!macro thread_safe_variable_comparison
-
#
-
# @see http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/AtomicReference.html
-
# @see http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html
-
#
-
# @!method initialize(value = nil)
-
# @!macro atomic_reference_method_initialize
-
# @param [Object] value The initial value.
-
#
-
# @!method get
-
# @!macro atomic_reference_method_get
-
# Gets the current value.
-
# @return [Object] the current value
-
#
-
# @!method set(new_value)
-
# @!macro atomic_reference_method_set
-
# Sets to the given value.
-
# @param [Object] new_value the new value
-
# @return [Object] the new value
-
#
-
# @!method get_and_set(new_value)
-
# @!macro atomic_reference_method_get_and_set
-
# Atomically sets to the given value and returns the old value.
-
# @param [Object] new_value the new value
-
# @return [Object] the old value
-
#
-
# @!method compare_and_set(old_value, new_value)
-
# @!macro atomic_reference_method_compare_and_set
-
#
-
# Atomically sets the value to the given updated value if
-
# the current value == the expected value.
-
#
-
# @param [Object] old_value the expected value
-
# @param [Object] new_value the new value
-
#
-
# @return [Boolean] `true` if successful. A `false` return indicates
-
# that the actual value was not equal to the expected value.
-
#
-
# @!method update
-
# @!macro atomic_reference_method_update
-
#
-
# @!method try_update
-
# @!macro atomic_reference_method_try_update
-
#
-
# @!method try_update!
-
# @!macro atomic_reference_method_try_update!
-
-
-
# @!macro internal_implementation_note
-
1
class ConcurrentUpdateError < ThreadError
-
# frozen pre-allocated backtrace to speed ConcurrentUpdateError
-
1
CONC_UP_ERR_BACKTRACE = ['backtrace elided; set verbose to enable'].freeze
-
end
-
-
# @!macro internal_implementation_note
-
AtomicReferenceImplementation = case
-
1
when Concurrent.on_cruby? && Concurrent.c_extensions_loaded?
-
# @!visibility private
-
# @!macro internal_implementation_note
-
class CAtomicReference
-
include AtomicDirectUpdate
-
include AtomicNumericCompareAndSetWrapper
-
alias_method :compare_and_swap, :compare_and_set
-
end
-
CAtomicReference
-
when Concurrent.on_jruby?
-
# @!visibility private
-
# @!macro internal_implementation_note
-
class JavaAtomicReference
-
include AtomicDirectUpdate
-
end
-
JavaAtomicReference
-
when Concurrent.on_truffleruby?
-
class TruffleRubyAtomicReference < TruffleRuby::AtomicReference
-
include AtomicDirectUpdate
-
alias_method :value, :get
-
alias_method :value=, :set
-
alias_method :compare_and_swap, :compare_and_set
-
alias_method :swap, :get_and_set
-
end
-
when Concurrent.on_rbx?
-
# @note Extends `Rubinius::AtomicReference` version adding aliases
-
# and numeric logic.
-
#
-
# @!visibility private
-
# @!macro internal_implementation_note
-
class RbxAtomicReference < Rubinius::AtomicReference
-
alias_method :_compare_and_set, :compare_and_set
-
include AtomicDirectUpdate
-
include AtomicNumericCompareAndSetWrapper
-
alias_method :value, :get
-
alias_method :value=, :set
-
alias_method :swap, :get_and_set
-
alias_method :compare_and_swap, :compare_and_set
-
end
-
RbxAtomicReference
-
else
-
1
MutexAtomicReference
-
end
-
1
private_constant :AtomicReferenceImplementation
-
-
# @!macro atomic_reference
-
1
class AtomicReference < AtomicReferenceImplementation
-
-
# @return [String] Short string representation.
-
1
def to_s
-
format '%s value:%s>', super[0..-2], get
-
end
-
-
1
alias_method :inspect, :to_s
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/atomic/mutex_count_down_latch'
-
1
require 'concurrent/atomic/java_count_down_latch'
-
-
1
module Concurrent
-
-
###################################################################
-
-
# @!macro count_down_latch_method_initialize
-
#
-
# Create a new `CountDownLatch` with the initial `count`.
-
#
-
# @param [new] count the initial count
-
#
-
# @raise [ArgumentError] if `count` is not an integer or is less than zero
-
-
# @!macro count_down_latch_method_wait
-
#
-
# Block on the latch until the counter reaches zero or until `timeout` is reached.
-
#
-
# @param [Fixnum] timeout the number of seconds to wait for the counter or `nil`
-
# to block indefinitely
-
# @return [Boolean] `true` if the `count` reaches zero else false on `timeout`
-
-
# @!macro count_down_latch_method_count_down
-
#
-
# Signal the latch to decrement the counter. Will signal all blocked threads when
-
# the `count` reaches zero.
-
-
# @!macro count_down_latch_method_count
-
#
-
# The current value of the counter.
-
#
-
# @return [Fixnum] the current value of the counter
-
-
###################################################################
-
-
# @!macro count_down_latch_public_api
-
#
-
# @!method initialize(count = 1)
-
# @!macro count_down_latch_method_initialize
-
#
-
# @!method wait(timeout = nil)
-
# @!macro count_down_latch_method_wait
-
#
-
# @!method count_down
-
# @!macro count_down_latch_method_count_down
-
#
-
# @!method count
-
# @!macro count_down_latch_method_count
-
-
###################################################################
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
CountDownLatchImplementation = case
-
1
when Concurrent.on_jruby?
-
JavaCountDownLatch
-
else
-
1
MutexCountDownLatch
-
end
-
1
private_constant :CountDownLatchImplementation
-
-
# @!macro count_down_latch
-
#
-
# A synchronization object that allows one thread to wait on multiple other threads.
-
# The thread that will wait creates a `CountDownLatch` and sets the initial value
-
# (normally equal to the number of other threads). The initiating thread passes the
-
# latch to the other threads then waits for the other threads by calling the `#wait`
-
# method. Each of the other threads calls `#count_down` when done with its work.
-
# When the latch counter reaches zero the waiting thread is unblocked and continues
-
# with its work. A `CountDownLatch` can be used only once. Its value cannot be reset.
-
#
-
# @!macro count_down_latch_public_api
-
# @example Waiter and Decrementer
-
# latch = Concurrent::CountDownLatch.new(3)
-
#
-
# waiter = Thread.new do
-
# latch.wait()
-
# puts ("Waiter released")
-
# end
-
#
-
# decrementer = Thread.new do
-
# sleep(1)
-
# latch.count_down
-
# puts latch.count
-
#
-
# sleep(1)
-
# latch.count_down
-
# puts latch.count
-
#
-
# sleep(1)
-
# latch.count_down
-
# puts latch.count
-
# end
-
#
-
# [waiter, decrementer].each(&:join)
-
1
class CountDownLatch < CountDownLatchImplementation
-
end
-
end
-
1
require 'concurrent/synchronization'
-
1
require 'concurrent/utility/native_integer'
-
-
1
module Concurrent
-
-
# A synchronization aid that allows a set of threads to all wait for each
-
# other to reach a common barrier point.
-
# @example
-
# barrier = Concurrent::CyclicBarrier.new(3)
-
# jobs = Array.new(3) { |i| -> { sleep i; p done: i } }
-
# process = -> (i) do
-
# # waiting to start at the same time
-
# barrier.wait
-
# # execute job
-
# jobs[i].call
-
# # wait for others to finish
-
# barrier.wait
-
# end
-
# threads = 2.times.map do |i|
-
# Thread.new(i, &process)
-
# end
-
#
-
# # use main as well
-
# process.call 2
-
#
-
# # here we can be sure that all jobs are processed
-
1
class CyclicBarrier < Synchronization::LockableObject
-
-
# @!visibility private
-
1
Generation = Struct.new(:status)
-
1
private_constant :Generation
-
-
# Create a new `CyclicBarrier` that waits for `parties` threads
-
#
-
# @param [Fixnum] parties the number of parties
-
# @yield an optional block that will be executed that will be executed after
-
# the last thread arrives and before the others are released
-
#
-
# @raise [ArgumentError] if `parties` is not an integer or is less than zero
-
1
def initialize(parties, &block)
-
Utility::NativeInteger.ensure_integer_and_bounds parties
-
Utility::NativeInteger.ensure_positive_and_no_zero parties
-
-
super(&nil)
-
synchronize { ns_initialize parties, &block }
-
end
-
-
# @return [Fixnum] the number of threads needed to pass the barrier
-
1
def parties
-
synchronize { @parties }
-
end
-
-
# @return [Fixnum] the number of threads currently waiting on the barrier
-
1
def number_waiting
-
synchronize { @number_waiting }
-
end
-
-
# Blocks on the barrier until the number of waiting threads is equal to
-
# `parties` or until `timeout` is reached or `reset` is called
-
# If a block has been passed to the constructor, it will be executed once by
-
# the last arrived thread before releasing the others
-
# @param [Fixnum] timeout the number of seconds to wait for the counter or
-
# `nil` to block indefinitely
-
# @return [Boolean] `true` if the `count` reaches zero else false on
-
# `timeout` or on `reset` or if the barrier is broken
-
1
def wait(timeout = nil)
-
synchronize do
-
-
return false unless @generation.status == :waiting
-
-
@number_waiting += 1
-
-
if @number_waiting == @parties
-
@action.call if @action
-
ns_generation_done @generation, :fulfilled
-
true
-
else
-
generation = @generation
-
if ns_wait_until(timeout) { generation.status != :waiting }
-
generation.status == :fulfilled
-
else
-
ns_generation_done generation, :broken, false
-
false
-
end
-
end
-
end
-
end
-
-
# resets the barrier to its initial state
-
# If there is at least one waiting thread, it will be woken up, the `wait`
-
# method will return false and the barrier will be broken
-
# If the barrier is broken, this method restores it to the original state
-
#
-
# @return [nil]
-
1
def reset
-
synchronize { ns_generation_done @generation, :reset }
-
end
-
-
# A barrier can be broken when:
-
# - a thread called the `reset` method while at least one other thread was waiting
-
# - at least one thread timed out on `wait` method
-
#
-
# A broken barrier can be restored using `reset` it's safer to create a new one
-
# @return [Boolean] true if the barrier is broken otherwise false
-
1
def broken?
-
synchronize { @generation.status != :waiting }
-
end
-
-
1
protected
-
-
1
def ns_generation_done(generation, status, continue = true)
-
generation.status = status
-
ns_next_generation if continue
-
ns_broadcast
-
end
-
-
1
def ns_next_generation
-
@generation = Generation.new(:waiting)
-
@number_waiting = 0
-
end
-
-
1
def ns_initialize(parties, &block)
-
@parties = parties
-
@action = block
-
ns_next_generation
-
end
-
end
-
end
-
1
if Concurrent.on_jruby?
-
-
module Concurrent
-
-
# @!macro count_down_latch
-
# @!visibility private
-
# @!macro internal_implementation_note
-
class JavaCountDownLatch
-
-
# @!macro count_down_latch_method_initialize
-
def initialize(count = 1)
-
Utility::NativeInteger.ensure_integer_and_bounds(count)
-
Utility::NativeInteger.ensure_positive(count)
-
@latch = java.util.concurrent.CountDownLatch.new(count)
-
end
-
-
# @!macro count_down_latch_method_wait
-
def wait(timeout = nil)
-
result = nil
-
if timeout.nil?
-
Synchronization::JRuby.sleep_interruptibly { @latch.await }
-
result = true
-
else
-
Synchronization::JRuby.sleep_interruptibly do
-
result = @latch.await(1000 * timeout, java.util.concurrent.TimeUnit::MILLISECONDS)
-
end
-
end
-
result
-
end
-
-
# @!macro count_down_latch_method_count_down
-
def count_down
-
@latch.countDown
-
end
-
-
# @!macro count_down_latch_method_count
-
def count
-
@latch.getCount
-
end
-
end
-
end
-
end
-
1
require 'concurrent/atomic/abstract_thread_local_var'
-
-
1
if Concurrent.on_jruby?
-
-
module Concurrent
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
class JavaThreadLocalVar < AbstractThreadLocalVar
-
-
# @!macro thread_local_var_method_get
-
def value
-
value = @var.get
-
-
if value.nil?
-
default
-
elsif value == NULL
-
nil
-
else
-
value
-
end
-
end
-
-
# @!macro thread_local_var_method_set
-
def value=(value)
-
@var.set(value)
-
end
-
-
protected
-
-
# @!visibility private
-
def allocate_storage
-
@var = java.lang.ThreadLocal.new
-
end
-
end
-
end
-
end
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# @!macro atomic_boolean
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class MutexAtomicBoolean < Synchronization::LockableObject
-
-
# @!macro atomic_boolean_method_initialize
-
1
def initialize(initial = false)
-
super()
-
synchronize { ns_initialize(initial) }
-
end
-
-
# @!macro atomic_boolean_method_value_get
-
1
def value
-
synchronize { @value }
-
end
-
-
# @!macro atomic_boolean_method_value_set
-
1
def value=(value)
-
synchronize { @value = !!value }
-
end
-
-
# @!macro atomic_boolean_method_true_question
-
1
def true?
-
synchronize { @value }
-
end
-
-
# @!macro atomic_boolean_method_false_question
-
1
def false?
-
synchronize { !@value }
-
end
-
-
# @!macro atomic_boolean_method_make_true
-
1
def make_true
-
synchronize { ns_make_value(true) }
-
end
-
-
# @!macro atomic_boolean_method_make_false
-
1
def make_false
-
synchronize { ns_make_value(false) }
-
end
-
-
1
protected
-
-
# @!visibility private
-
1
def ns_initialize(initial)
-
@value = !!initial
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def ns_make_value(value)
-
old = @value
-
@value = value
-
old != @value
-
end
-
end
-
end
-
1
require 'concurrent/synchronization'
-
1
require 'concurrent/utility/native_integer'
-
-
1
module Concurrent
-
-
# @!macro atomic_fixnum
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class MutexAtomicFixnum < Synchronization::LockableObject
-
-
# @!macro atomic_fixnum_method_initialize
-
1
def initialize(initial = 0)
-
super()
-
synchronize { ns_initialize(initial) }
-
end
-
-
# @!macro atomic_fixnum_method_value_get
-
1
def value
-
synchronize { @value }
-
end
-
-
# @!macro atomic_fixnum_method_value_set
-
1
def value=(value)
-
synchronize { ns_set(value) }
-
end
-
-
# @!macro atomic_fixnum_method_increment
-
1
def increment(delta = 1)
-
synchronize { ns_set(@value + delta.to_i) }
-
end
-
-
1
alias_method :up, :increment
-
-
# @!macro atomic_fixnum_method_decrement
-
1
def decrement(delta = 1)
-
synchronize { ns_set(@value - delta.to_i) }
-
end
-
-
1
alias_method :down, :decrement
-
-
# @!macro atomic_fixnum_method_compare_and_set
-
1
def compare_and_set(expect, update)
-
synchronize do
-
if @value == expect.to_i
-
@value = update.to_i
-
true
-
else
-
false
-
end
-
end
-
end
-
-
# @!macro atomic_fixnum_method_update
-
1
def update
-
synchronize do
-
@value = yield @value
-
end
-
end
-
-
1
protected
-
-
# @!visibility private
-
1
def ns_initialize(initial)
-
ns_set(initial)
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def ns_set(value)
-
Utility::NativeInteger.ensure_integer_and_bounds value
-
@value = value
-
end
-
end
-
end
-
1
require 'concurrent/synchronization'
-
1
require 'concurrent/utility/native_integer'
-
-
1
module Concurrent
-
-
# @!macro count_down_latch
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class MutexCountDownLatch < Synchronization::LockableObject
-
-
# @!macro count_down_latch_method_initialize
-
1
def initialize(count = 1)
-
Utility::NativeInteger.ensure_integer_and_bounds count
-
Utility::NativeInteger.ensure_positive count
-
-
super()
-
synchronize { ns_initialize count }
-
end
-
-
# @!macro count_down_latch_method_wait
-
1
def wait(timeout = nil)
-
synchronize { ns_wait_until(timeout) { @count == 0 } }
-
end
-
-
# @!macro count_down_latch_method_count_down
-
1
def count_down
-
synchronize do
-
@count -= 1 if @count > 0
-
ns_broadcast if @count == 0
-
end
-
end
-
-
# @!macro count_down_latch_method_count
-
1
def count
-
synchronize { @count }
-
end
-
-
1
protected
-
-
1
def ns_initialize(count)
-
@count = count
-
end
-
end
-
end
-
1
require 'thread'
-
1
require 'concurrent/atomic/atomic_fixnum'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# Ruby read-write lock implementation
-
#
-
# Allows any number of concurrent readers, but only one concurrent writer
-
# (And if the "write" lock is taken, any readers who come along will have to wait)
-
#
-
# If readers are already active when a writer comes along, the writer will wait for
-
# all the readers to finish before going ahead.
-
# Any additional readers that come when the writer is already waiting, will also
-
# wait (so writers are not starved).
-
#
-
# This implementation is based on `java.util.concurrent.ReentrantReadWriteLock`.
-
#
-
# @example
-
# lock = Concurrent::ReadWriteLock.new
-
# lock.with_read_lock { data.retrieve }
-
# lock.with_write_lock { data.modify! }
-
#
-
# @note Do **not** try to acquire the write lock while already holding a read lock
-
# **or** try to acquire the write lock while you already have it.
-
# This will lead to deadlock
-
#
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html java.util.concurrent.ReentrantReadWriteLock
-
1
class ReadWriteLock < Synchronization::Object
-
-
# @!visibility private
-
1
WAITING_WRITER = 1 << 15
-
-
# @!visibility private
-
1
RUNNING_WRITER = 1 << 29
-
-
# @!visibility private
-
1
MAX_READERS = WAITING_WRITER - 1
-
-
# @!visibility private
-
1
MAX_WRITERS = RUNNING_WRITER - MAX_READERS - 1
-
-
1
safe_initialization!
-
-
# Implementation notes:
-
# A goal is to make the uncontended path for both readers/writers lock-free
-
# Only if there is reader-writer or writer-writer contention, should locks be used
-
# Internal state is represented by a single integer ("counter"), and updated
-
# using atomic compare-and-swap operations
-
# When the counter is 0, the lock is free
-
# Each reader increments the counter by 1 when acquiring a read lock
-
# (and decrements by 1 when releasing the read lock)
-
# The counter is increased by (1 << 15) for each writer waiting to acquire the
-
# write lock, and by (1 << 29) if the write lock is taken
-
-
# Create a new `ReadWriteLock` in the unlocked state.
-
1
def initialize
-
super()
-
@Counter = AtomicFixnum.new(0) # single integer which represents lock state
-
@ReadLock = Synchronization::Lock.new
-
@WriteLock = Synchronization::Lock.new
-
end
-
-
# Execute a block operation within a read lock.
-
#
-
# @yield the task to be performed within the lock.
-
#
-
# @return [Object] the result of the block operation.
-
#
-
# @raise [ArgumentError] when no block is given.
-
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
-
# is exceeded.
-
1
def with_read_lock
-
raise ArgumentError.new('no block given') unless block_given?
-
acquire_read_lock
-
begin
-
yield
-
ensure
-
release_read_lock
-
end
-
end
-
-
# Execute a block operation within a write lock.
-
#
-
# @yield the task to be performed within the lock.
-
#
-
# @return [Object] the result of the block operation.
-
#
-
# @raise [ArgumentError] when no block is given.
-
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
-
# is exceeded.
-
1
def with_write_lock
-
raise ArgumentError.new('no block given') unless block_given?
-
acquire_write_lock
-
begin
-
yield
-
ensure
-
release_write_lock
-
end
-
end
-
-
# Acquire a read lock. If a write lock has been acquired will block until
-
# it is released. Will not block if other read locks have been acquired.
-
#
-
# @return [Boolean] true if the lock is successfully acquired
-
#
-
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
-
# is exceeded.
-
1
def acquire_read_lock
-
while true
-
c = @Counter.value
-
raise ResourceLimitError.new('Too many reader threads') if max_readers?(c)
-
-
# If a writer is waiting when we first queue up, we need to wait
-
if waiting_writer?(c)
-
@ReadLock.wait_until { !waiting_writer? }
-
-
# after a reader has waited once, they are allowed to "barge" ahead of waiting writers
-
# but if a writer is *running*, the reader still needs to wait (naturally)
-
while true
-
c = @Counter.value
-
if running_writer?(c)
-
@ReadLock.wait_until { !running_writer? }
-
else
-
return if @Counter.compare_and_set(c, c+1)
-
end
-
end
-
else
-
break if @Counter.compare_and_set(c, c+1)
-
end
-
end
-
true
-
end
-
-
# Release a previously acquired read lock.
-
#
-
# @return [Boolean] true if the lock is successfully released
-
1
def release_read_lock
-
while true
-
c = @Counter.value
-
if @Counter.compare_and_set(c, c-1)
-
# If one or more writers were waiting, and we were the last reader, wake a writer up
-
if waiting_writer?(c) && running_readers(c) == 1
-
@WriteLock.signal
-
end
-
break
-
end
-
end
-
true
-
end
-
-
# Acquire a write lock. Will block and wait for all active readers and writers.
-
#
-
# @return [Boolean] true if the lock is successfully acquired
-
#
-
# @raise [Concurrent::ResourceLimitError] if the maximum number of writers
-
# is exceeded.
-
1
def acquire_write_lock
-
while true
-
c = @Counter.value
-
raise ResourceLimitError.new('Too many writer threads') if max_writers?(c)
-
-
if c == 0 # no readers OR writers running
-
# if we successfully swap the RUNNING_WRITER bit on, then we can go ahead
-
break if @Counter.compare_and_set(0, RUNNING_WRITER)
-
elsif @Counter.compare_and_set(c, c+WAITING_WRITER)
-
while true
-
# Now we have successfully incremented, so no more readers will be able to increment
-
# (they will wait instead)
-
# However, readers OR writers could decrement right here, OR another writer could increment
-
@WriteLock.wait_until do
-
# So we have to do another check inside the synchronized section
-
# If a writer OR reader is running, then go to sleep
-
c = @Counter.value
-
!running_writer?(c) && !running_readers?(c)
-
end
-
-
# We just came out of a wait
-
# If we successfully turn the RUNNING_WRITER bit on with an atomic swap,
-
# Then we are OK to stop waiting and go ahead
-
# Otherwise go back and wait again
-
c = @Counter.value
-
break if !running_writer?(c) && !running_readers?(c) && @Counter.compare_and_set(c, c+RUNNING_WRITER-WAITING_WRITER)
-
end
-
break
-
end
-
end
-
true
-
end
-
-
# Release a previously acquired write lock.
-
#
-
# @return [Boolean] true if the lock is successfully released
-
1
def release_write_lock
-
return true unless running_writer?
-
c = @Counter.update { |counter| counter - RUNNING_WRITER }
-
@ReadLock.broadcast
-
@WriteLock.signal if waiting_writers(c) > 0
-
true
-
end
-
-
# Queries if the write lock is held by any thread.
-
#
-
# @return [Boolean] true if the write lock is held else false`
-
1
def write_locked?
-
@Counter.value >= RUNNING_WRITER
-
end
-
-
# Queries whether any threads are waiting to acquire the read or write lock.
-
#
-
# @return [Boolean] true if any threads are waiting for a lock else false
-
1
def has_waiters?
-
waiting_writer?(@Counter.value)
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def running_readers(c = @Counter.value)
-
c & MAX_READERS
-
end
-
-
# @!visibility private
-
1
def running_readers?(c = @Counter.value)
-
(c & MAX_READERS) > 0
-
end
-
-
# @!visibility private
-
1
def running_writer?(c = @Counter.value)
-
c >= RUNNING_WRITER
-
end
-
-
# @!visibility private
-
1
def waiting_writers(c = @Counter.value)
-
(c & MAX_WRITERS) / WAITING_WRITER
-
end
-
-
# @!visibility private
-
1
def waiting_writer?(c = @Counter.value)
-
c >= WAITING_WRITER
-
end
-
-
# @!visibility private
-
1
def max_readers?(c = @Counter.value)
-
(c & MAX_READERS) == MAX_READERS
-
end
-
-
# @!visibility private
-
1
def max_writers?(c = @Counter.value)
-
(c & MAX_WRITERS) == MAX_WRITERS
-
end
-
end
-
end
-
1
require 'thread'
-
1
require 'concurrent/atomic/atomic_reference'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/synchronization'
-
1
require 'concurrent/atomic/thread_local_var'
-
-
1
module Concurrent
-
-
# Re-entrant read-write lock implementation
-
#
-
# Allows any number of concurrent readers, but only one concurrent writer
-
# (And while the "write" lock is taken, no read locks can be obtained either.
-
# Hence, the write lock can also be called an "exclusive" lock.)
-
#
-
# If another thread has taken a read lock, any thread which wants a write lock
-
# will block until all the readers release their locks. However, once a thread
-
# starts waiting to obtain a write lock, any additional readers that come along
-
# will also wait (so writers are not starved).
-
#
-
# A thread can acquire both a read and write lock at the same time. A thread can
-
# also acquire a read lock OR a write lock more than once. Only when the read (or
-
# write) lock is released as many times as it was acquired, will the thread
-
# actually let it go, allowing other threads which might have been waiting
-
# to proceed. Therefore the lock can be upgraded by first acquiring
-
# read lock and then write lock and that the lock can be downgraded by first
-
# having both read and write lock a releasing just the write lock.
-
#
-
# If both read and write locks are acquired by the same thread, it is not strictly
-
# necessary to release them in the same order they were acquired. In other words,
-
# the following code is legal:
-
#
-
# @example
-
# lock = Concurrent::ReentrantReadWriteLock.new
-
# lock.acquire_write_lock
-
# lock.acquire_read_lock
-
# lock.release_write_lock
-
# # At this point, the current thread is holding only a read lock, not a write
-
# # lock. So other threads can take read locks, but not a write lock.
-
# lock.release_read_lock
-
# # Now the current thread is not holding either a read or write lock, so
-
# # another thread could potentially acquire a write lock.
-
#
-
# This implementation was inspired by `java.util.concurrent.ReentrantReadWriteLock`.
-
#
-
# @example
-
# lock = Concurrent::ReentrantReadWriteLock.new
-
# lock.with_read_lock { data.retrieve }
-
# lock.with_write_lock { data.modify! }
-
#
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html java.util.concurrent.ReentrantReadWriteLock
-
1
class ReentrantReadWriteLock < Synchronization::Object
-
-
# Implementation notes:
-
#
-
# A goal is to make the uncontended path for both readers/writers mutex-free
-
# Only if there is reader-writer or writer-writer contention, should mutexes be used
-
# Otherwise, a single CAS operation is all we need to acquire/release a lock
-
#
-
# Internal state is represented by a single integer ("counter"), and updated
-
# using atomic compare-and-swap operations
-
# When the counter is 0, the lock is free
-
# Each thread which has one OR MORE read locks increments the counter by 1
-
# (and decrements by 1 when releasing the read lock)
-
# The counter is increased by (1 << 15) for each writer waiting to acquire the
-
# write lock, and by (1 << 29) if the write lock is taken
-
#
-
# Additionally, each thread uses a thread-local variable to count how many times
-
# it has acquired a read lock, AND how many times it has acquired a write lock.
-
# It uses a similar trick; an increment of 1 means a read lock was taken, and
-
# an increment of (1 << 15) means a write lock was taken
-
# This is what makes re-entrancy possible
-
#
-
# 2 rules are followed to ensure good liveness properties:
-
# 1) Once a writer has queued up and is waiting for a write lock, no other thread
-
# can take a lock without waiting
-
# 2) When a write lock is released, readers are given the "first chance" to wake
-
# up and acquire a read lock
-
# Following these rules means readers and writers tend to "take turns", so neither
-
# can starve the other, even under heavy contention
-
-
# @!visibility private
-
1
READER_BITS = 15
-
# @!visibility private
-
1
WRITER_BITS = 14
-
-
# Used with @Counter:
-
# @!visibility private
-
1
WAITING_WRITER = 1 << READER_BITS
-
# @!visibility private
-
1
RUNNING_WRITER = 1 << (READER_BITS + WRITER_BITS)
-
# @!visibility private
-
1
MAX_READERS = WAITING_WRITER - 1
-
# @!visibility private
-
1
MAX_WRITERS = RUNNING_WRITER - MAX_READERS - 1
-
-
# Used with @HeldCount:
-
# @!visibility private
-
1
WRITE_LOCK_HELD = 1 << READER_BITS
-
# @!visibility private
-
1
READ_LOCK_MASK = WRITE_LOCK_HELD - 1
-
# @!visibility private
-
1
WRITE_LOCK_MASK = MAX_WRITERS
-
-
1
safe_initialization!
-
-
# Create a new `ReentrantReadWriteLock` in the unlocked state.
-
1
def initialize
-
super()
-
@Counter = AtomicFixnum.new(0) # single integer which represents lock state
-
@ReadQueue = Synchronization::Lock.new # used to queue waiting readers
-
@WriteQueue = Synchronization::Lock.new # used to queue waiting writers
-
@HeldCount = ThreadLocalVar.new(0) # indicates # of R & W locks held by this thread
-
end
-
-
# Execute a block operation within a read lock.
-
#
-
# @yield the task to be performed within the lock.
-
#
-
# @return [Object] the result of the block operation.
-
#
-
# @raise [ArgumentError] when no block is given.
-
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
-
# is exceeded.
-
1
def with_read_lock
-
raise ArgumentError.new('no block given') unless block_given?
-
acquire_read_lock
-
begin
-
yield
-
ensure
-
release_read_lock
-
end
-
end
-
-
# Execute a block operation within a write lock.
-
#
-
# @yield the task to be performed within the lock.
-
#
-
# @return [Object] the result of the block operation.
-
#
-
# @raise [ArgumentError] when no block is given.
-
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
-
# is exceeded.
-
1
def with_write_lock
-
raise ArgumentError.new('no block given') unless block_given?
-
acquire_write_lock
-
begin
-
yield
-
ensure
-
release_write_lock
-
end
-
end
-
-
# Acquire a read lock. If a write lock is held by another thread, will block
-
# until it is released.
-
#
-
# @return [Boolean] true if the lock is successfully acquired
-
#
-
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
-
# is exceeded.
-
1
def acquire_read_lock
-
if (held = @HeldCount.value) > 0
-
# If we already have a lock, there's no need to wait
-
if held & READ_LOCK_MASK == 0
-
# But we do need to update the counter, if we were holding a write
-
# lock but not a read lock
-
@Counter.update { |c| c + 1 }
-
end
-
@HeldCount.value = held + 1
-
return true
-
end
-
-
while true
-
c = @Counter.value
-
raise ResourceLimitError.new('Too many reader threads') if max_readers?(c)
-
-
# If a writer is waiting OR running when we first queue up, we need to wait
-
if waiting_or_running_writer?(c)
-
# Before going to sleep, check again with the ReadQueue mutex held
-
@ReadQueue.synchronize do
-
@ReadQueue.ns_wait if waiting_or_running_writer?
-
end
-
# Note: the above 'synchronize' block could have used #wait_until,
-
# but that waits repeatedly in a loop, checking the wait condition
-
# each time it wakes up (to protect against spurious wakeups)
-
# But we are already in a loop, which is only broken when we successfully
-
# acquire the lock! So we don't care about spurious wakeups, and would
-
# rather not pay the extra overhead of using #wait_until
-
-
# After a reader has waited once, they are allowed to "barge" ahead of waiting writers
-
# But if a writer is *running*, the reader still needs to wait (naturally)
-
while true
-
c = @Counter.value
-
if running_writer?(c)
-
@ReadQueue.synchronize do
-
@ReadQueue.ns_wait if running_writer?
-
end
-
elsif @Counter.compare_and_set(c, c+1)
-
@HeldCount.value = held + 1
-
return true
-
end
-
end
-
elsif @Counter.compare_and_set(c, c+1)
-
@HeldCount.value = held + 1
-
return true
-
end
-
end
-
end
-
-
# Try to acquire a read lock and return true if we succeed. If it cannot be
-
# acquired immediately, return false.
-
#
-
# @return [Boolean] true if the lock is successfully acquired
-
1
def try_read_lock
-
if (held = @HeldCount.value) > 0
-
if held & READ_LOCK_MASK == 0
-
# If we hold a write lock, but not a read lock...
-
@Counter.update { |c| c + 1 }
-
end
-
@HeldCount.value = held + 1
-
return true
-
else
-
c = @Counter.value
-
if !waiting_or_running_writer?(c) && @Counter.compare_and_set(c, c+1)
-
@HeldCount.value = held + 1
-
return true
-
end
-
end
-
false
-
end
-
-
# Release a previously acquired read lock.
-
#
-
# @return [Boolean] true if the lock is successfully released
-
1
def release_read_lock
-
held = @HeldCount.value = @HeldCount.value - 1
-
rlocks_held = held & READ_LOCK_MASK
-
if rlocks_held == 0
-
c = @Counter.update { |counter| counter - 1 }
-
# If one or more writers were waiting, and we were the last reader, wake a writer up
-
if waiting_or_running_writer?(c) && running_readers(c) == 0
-
@WriteQueue.signal
-
end
-
elsif rlocks_held == READ_LOCK_MASK
-
raise IllegalOperationError, "Cannot release a read lock which is not held"
-
end
-
true
-
end
-
-
# Acquire a write lock. Will block and wait for all active readers and writers.
-
#
-
# @return [Boolean] true if the lock is successfully acquired
-
#
-
# @raise [Concurrent::ResourceLimitError] if the maximum number of writers
-
# is exceeded.
-
1
def acquire_write_lock
-
if (held = @HeldCount.value) >= WRITE_LOCK_HELD
-
# if we already have a write (exclusive) lock, there's no need to wait
-
@HeldCount.value = held + WRITE_LOCK_HELD
-
return true
-
end
-
-
while true
-
c = @Counter.value
-
raise ResourceLimitError.new('Too many writer threads') if max_writers?(c)
-
-
# To go ahead and take the lock without waiting, there must be no writer
-
# running right now, AND no writers who came before us still waiting to
-
# acquire the lock
-
# Additionally, if any read locks have been taken, we must hold all of them
-
if c == held
-
# If we successfully swap the RUNNING_WRITER bit on, then we can go ahead
-
if @Counter.compare_and_set(c, c+RUNNING_WRITER)
-
@HeldCount.value = held + WRITE_LOCK_HELD
-
return true
-
end
-
elsif @Counter.compare_and_set(c, c+WAITING_WRITER)
-
while true
-
# Now we have successfully incremented, so no more readers will be able to increment
-
# (they will wait instead)
-
# However, readers OR writers could decrement right here
-
@WriteQueue.synchronize do
-
# So we have to do another check inside the synchronized section
-
# If a writer OR another reader is running, then go to sleep
-
c = @Counter.value
-
@WriteQueue.ns_wait if running_writer?(c) || running_readers(c) != held
-
end
-
# Note: if you are thinking of replacing the above 'synchronize' block
-
# with #wait_until, read the comment in #acquire_read_lock first!
-
-
# We just came out of a wait
-
# If we successfully turn the RUNNING_WRITER bit on with an atomic swap,
-
# then we are OK to stop waiting and go ahead
-
# Otherwise go back and wait again
-
c = @Counter.value
-
if !running_writer?(c) &&
-
running_readers(c) == held &&
-
@Counter.compare_and_set(c, c+RUNNING_WRITER-WAITING_WRITER)
-
@HeldCount.value = held + WRITE_LOCK_HELD
-
return true
-
end
-
end
-
end
-
end
-
end
-
-
# Try to acquire a write lock and return true if we succeed. If it cannot be
-
# acquired immediately, return false.
-
#
-
# @return [Boolean] true if the lock is successfully acquired
-
1
def try_write_lock
-
if (held = @HeldCount.value) >= WRITE_LOCK_HELD
-
@HeldCount.value = held + WRITE_LOCK_HELD
-
return true
-
else
-
c = @Counter.value
-
if !waiting_or_running_writer?(c) &&
-
running_readers(c) == held &&
-
@Counter.compare_and_set(c, c+RUNNING_WRITER)
-
@HeldCount.value = held + WRITE_LOCK_HELD
-
return true
-
end
-
end
-
false
-
end
-
-
# Release a previously acquired write lock.
-
#
-
# @return [Boolean] true if the lock is successfully released
-
1
def release_write_lock
-
held = @HeldCount.value = @HeldCount.value - WRITE_LOCK_HELD
-
wlocks_held = held & WRITE_LOCK_MASK
-
if wlocks_held == 0
-
c = @Counter.update { |counter| counter - RUNNING_WRITER }
-
@ReadQueue.broadcast
-
@WriteQueue.signal if waiting_writers(c) > 0
-
elsif wlocks_held == WRITE_LOCK_MASK
-
raise IllegalOperationError, "Cannot release a write lock which is not held"
-
end
-
true
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def running_readers(c = @Counter.value)
-
c & MAX_READERS
-
end
-
-
# @!visibility private
-
1
def running_readers?(c = @Counter.value)
-
(c & MAX_READERS) > 0
-
end
-
-
# @!visibility private
-
1
def running_writer?(c = @Counter.value)
-
c >= RUNNING_WRITER
-
end
-
-
# @!visibility private
-
1
def waiting_writers(c = @Counter.value)
-
(c & MAX_WRITERS) >> READER_BITS
-
end
-
-
# @!visibility private
-
1
def waiting_or_running_writer?(c = @Counter.value)
-
c >= WAITING_WRITER
-
end
-
-
# @!visibility private
-
1
def max_readers?(c = @Counter.value)
-
(c & MAX_READERS) == MAX_READERS
-
end
-
-
# @!visibility private
-
1
def max_writers?(c = @Counter.value)
-
(c & MAX_WRITERS) == MAX_WRITERS
-
end
-
end
-
end
-
1
require 'thread'
-
1
require 'concurrent/atomic/abstract_thread_local_var'
-
-
1
module Concurrent
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class RubyThreadLocalVar < AbstractThreadLocalVar
-
-
# Each thread has a (lazily initialized) array of thread-local variable values
-
# Each time a new thread-local var is created, we allocate an "index" for it
-
# For example, if the allocated index is 1, that means slot #1 in EVERY
-
# thread's thread-local array will be used for the value of that TLV
-
#
-
# The good thing about using a per-THREAD structure to hold values, rather
-
# than a per-TLV structure, is that no synchronization is needed when
-
# reading and writing those values (since the structure is only ever
-
# accessed by a single thread)
-
#
-
# Of course, when a TLV is GC'd, 1) we need to recover its index for use
-
# by other new TLVs (otherwise the thread-local arrays could get bigger
-
# and bigger with time), and 2) we need to null out all the references
-
# held in the now-unused slots (both to avoid blocking GC of those objects,
-
# and also to prevent "stale" values from being passed on to a new TLV
-
# when the index is reused)
-
# Because we need to null out freed slots, we need to keep references to
-
# ALL the thread-local arrays -- ARRAYS is for that
-
# But when a Thread is GC'd, we need to drop the reference to its thread-local
-
# array, so we don't leak memory
-
-
# @!visibility private
-
1
FREE = []
-
1
LOCK = Mutex.new
-
1
ARRAYS = {} # used as a hash set
-
# noinspection RubyClassVariableUsageInspection
-
1
@@next = 0
-
1
QUEUE = Queue.new
-
1
THREAD = Thread.new do
-
1
while true
-
1
method, i = QUEUE.pop
-
case method
-
when :thread_local_finalizer
-
LOCK.synchronize do
-
FREE.push(i)
-
# The cost of GC'ing a TLV is linear in the number of threads using TLVs
-
# But that is natural! More threads means more storage is used per TLV
-
# So naturally more CPU time is required to free more storage
-
ARRAYS.each_value do |array|
-
array[i] = nil
-
end
-
end
-
when :thread_finalizer
-
LOCK.synchronize do
-
# The thread which used this thread-local array is now gone
-
# So don't hold onto a reference to the array (thus blocking GC)
-
ARRAYS.delete(i)
-
end
-
end
-
end
-
end
-
-
1
private_constant :FREE, :LOCK, :ARRAYS, :QUEUE, :THREAD
-
-
# @!macro thread_local_var_method_get
-
1
def value
-
if (array = get_threadlocal_array)
-
value = array[@index]
-
if value.nil?
-
default
-
elsif value.equal?(NULL)
-
nil
-
else
-
value
-
end
-
else
-
default
-
end
-
end
-
-
# @!macro thread_local_var_method_set
-
1
def value=(value)
-
me = Thread.current
-
# We could keep the thread-local arrays in a hash, keyed by Thread
-
# But why? That would require locking
-
# Using Ruby's built-in thread-local storage is faster
-
unless (array = get_threadlocal_array(me))
-
array = set_threadlocal_array([], me)
-
LOCK.synchronize { ARRAYS[array.object_id] = array }
-
ObjectSpace.define_finalizer(me, self.class.thread_finalizer(array.object_id))
-
end
-
array[@index] = (value.nil? ? NULL : value)
-
value
-
end
-
-
1
protected
-
-
# @!visibility private
-
# noinspection RubyClassVariableUsageInspection
-
1
def allocate_storage
-
@index = LOCK.synchronize do
-
FREE.pop || begin
-
result = @@next
-
@@next += 1
-
result
-
end
-
end
-
ObjectSpace.define_finalizer(self, self.class.thread_local_finalizer(@index))
-
end
-
-
# @!visibility private
-
1
def self.thread_local_finalizer(index)
-
# avoid error: can't be called from trap context
-
proc { QUEUE.push [:thread_local_finalizer, index] }
-
end
-
-
# @!visibility private
-
1
def self.thread_finalizer(id)
-
# avoid error: can't be called from trap context
-
proc { QUEUE.push [:thread_finalizer, id] }
-
end
-
-
1
private
-
-
1
if Thread.instance_methods.include?(:thread_variable_get)
-
-
1
def get_threadlocal_array(thread = Thread.current)
-
thread.thread_variable_get(:__threadlocal_array__)
-
end
-
-
1
def set_threadlocal_array(array, thread = Thread.current)
-
thread.thread_variable_set(:__threadlocal_array__, array)
-
end
-
-
else
-
-
def get_threadlocal_array(thread = Thread.current)
-
thread[:__threadlocal_array__]
-
end
-
-
def set_threadlocal_array(array, thread = Thread.current)
-
thread[:__threadlocal_array__] = array
-
end
-
end
-
-
# This exists only for use in testing
-
# @!visibility private
-
1
def value_for(thread)
-
if (array = get_threadlocal_array(thread))
-
value = array[@index]
-
if value.nil?
-
get_default
-
elsif value.equal?(NULL)
-
nil
-
else
-
value
-
end
-
else
-
get_default
-
end
-
end
-
-
# @!visibility private
-
1
def get_default
-
if @default_block
-
raise "Cannot use default_for with default block"
-
else
-
@default
-
end
-
end
-
end
-
end
-
1
require 'concurrent/atomic/mutex_semaphore'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
###################################################################
-
-
# @!macro semaphore_method_initialize
-
#
-
# Create a new `Semaphore` with the initial `count`.
-
#
-
# @param [Fixnum] count the initial count
-
#
-
# @raise [ArgumentError] if `count` is not an integer or is less than zero
-
-
# @!macro semaphore_method_acquire
-
#
-
# Acquires the given number of permits from this semaphore,
-
# blocking until all are available.
-
#
-
# @param [Fixnum] permits Number of permits to acquire
-
#
-
# @raise [ArgumentError] if `permits` is not an integer or is less than
-
# one
-
#
-
# @return [nil]
-
-
# @!macro semaphore_method_available_permits
-
#
-
# Returns the current number of permits available in this semaphore.
-
#
-
# @return [Integer]
-
-
# @!macro semaphore_method_drain_permits
-
#
-
# Acquires and returns all permits that are immediately available.
-
#
-
# @return [Integer]
-
-
# @!macro semaphore_method_try_acquire
-
#
-
# Acquires the given number of permits from this semaphore,
-
# only if all are available at the time of invocation or within
-
# `timeout` interval
-
#
-
# @param [Fixnum] permits the number of permits to acquire
-
#
-
# @param [Fixnum] timeout the number of seconds to wait for the counter
-
# or `nil` to return immediately
-
#
-
# @raise [ArgumentError] if `permits` is not an integer or is less than
-
# one
-
#
-
# @return [Boolean] `false` if no permits are available, `true` when
-
# acquired a permit
-
-
# @!macro semaphore_method_release
-
#
-
# Releases the given number of permits, returning them to the semaphore.
-
#
-
# @param [Fixnum] permits Number of permits to return to the semaphore.
-
#
-
# @raise [ArgumentError] if `permits` is not a number or is less than one
-
#
-
# @return [nil]
-
-
###################################################################
-
-
# @!macro semaphore_public_api
-
#
-
# @!method initialize(count)
-
# @!macro semaphore_method_initialize
-
#
-
# @!method acquire(permits = 1)
-
# @!macro semaphore_method_acquire
-
#
-
# @!method available_permits
-
# @!macro semaphore_method_available_permits
-
#
-
# @!method drain_permits
-
# @!macro semaphore_method_drain_permits
-
#
-
# @!method try_acquire(permits = 1, timeout = nil)
-
# @!macro semaphore_method_try_acquire
-
#
-
# @!method release(permits = 1)
-
# @!macro semaphore_method_release
-
-
###################################################################
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
SemaphoreImplementation = case
-
1
when defined?(JavaSemaphore)
-
JavaSemaphore
-
else
-
1
MutexSemaphore
-
end
-
1
private_constant :SemaphoreImplementation
-
-
# @!macro semaphore
-
#
-
# A counting semaphore. Conceptually, a semaphore maintains a set of
-
# permits. Each {#acquire} blocks if necessary until a permit is
-
# available, and then takes it. Each {#release} adds a permit, potentially
-
# releasing a blocking acquirer.
-
# However, no actual permit objects are used; the Semaphore just keeps a
-
# count of the number available and acts accordingly.
-
#
-
# @!macro semaphore_public_api
-
# @example
-
# semaphore = Concurrent::Semaphore.new(2)
-
#
-
# t1 = Thread.new do
-
# semaphore.acquire
-
# puts "Thread 1 acquired semaphore"
-
# end
-
#
-
# t2 = Thread.new do
-
# semaphore.acquire
-
# puts "Thread 2 acquired semaphore"
-
# end
-
#
-
# t3 = Thread.new do
-
# semaphore.acquire
-
# puts "Thread 3 acquired semaphore"
-
# end
-
#
-
# t4 = Thread.new do
-
# sleep(2)
-
# puts "Thread 4 releasing semaphore"
-
# semaphore.release
-
# end
-
#
-
# [t1, t2, t3, t4].each(&:join)
-
#
-
# # prints:
-
# # Thread 3 acquired semaphore
-
# # Thread 2 acquired semaphore
-
# # Thread 4 releasing semaphore
-
# # Thread 1 acquired semaphore
-
#
-
1
class Semaphore < SemaphoreImplementation
-
end
-
end
-
1
module Concurrent
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class MutexAtomicReference < Synchronization::LockableObject
-
1
include AtomicDirectUpdate
-
1
include AtomicNumericCompareAndSetWrapper
-
1
alias_method :compare_and_swap, :compare_and_set
-
-
# @!macro atomic_reference_method_initialize
-
1
def initialize(value = nil)
-
1
super()
-
2
synchronize { ns_initialize(value) }
-
end
-
-
# @!macro atomic_reference_method_get
-
1
def get
-
synchronize { @value }
-
end
-
1
alias_method :value, :get
-
-
# @!macro atomic_reference_method_set
-
1
def set(new_value)
-
synchronize { @value = new_value }
-
end
-
1
alias_method :value=, :set
-
-
# @!macro atomic_reference_method_get_and_set
-
1
def get_and_set(new_value)
-
synchronize do
-
old_value = @value
-
@value = new_value
-
old_value
-
end
-
end
-
1
alias_method :swap, :get_and_set
-
-
# @!macro atomic_reference_method_compare_and_set
-
1
def _compare_and_set(old_value, new_value)
-
synchronize do
-
if @value.equal? old_value
-
@value = new_value
-
true
-
else
-
false
-
end
-
end
-
end
-
-
1
protected
-
-
1
def ns_initialize(value)
-
1
@value = value
-
end
-
end
-
end
-
1
module Concurrent
-
-
# Special "compare and set" handling of numeric values.
-
#
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
module AtomicNumericCompareAndSetWrapper
-
-
# @!macro atomic_reference_method_compare_and_set
-
1
def compare_and_set(old_value, new_value)
-
if old_value.kind_of? Numeric
-
while true
-
old = get
-
-
return false unless old.kind_of? Numeric
-
-
return false unless old == old_value
-
-
result = _compare_and_set(old, new_value)
-
return result if result
-
end
-
else
-
_compare_and_set(old_value, new_value)
-
end
-
end
-
-
end
-
end
-
1
require 'concurrent/atomic/atomic_reference'
-
1
require 'concurrent/atomic/atomic_boolean'
-
1
require 'concurrent/atomic/atomic_fixnum'
-
1
require 'concurrent/atomic/cyclic_barrier'
-
1
require 'concurrent/atomic/count_down_latch'
-
1
require 'concurrent/atomic/event'
-
1
require 'concurrent/atomic/read_write_lock'
-
1
require 'concurrent/atomic/reentrant_read_write_lock'
-
1
require 'concurrent/atomic/semaphore'
-
1
require 'concurrent/atomic/thread_local_var'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
1
module Collection
-
-
# A thread safe observer set implemented using copy-on-read approach:
-
# observers are added and removed from a thread safe collection; every time
-
# a notification is required the internal data structure is copied to
-
# prevent concurrency issues
-
#
-
# @api private
-
1
class CopyOnNotifyObserverSet < Synchronization::LockableObject
-
-
1
def initialize
-
super()
-
synchronize { ns_initialize }
-
end
-
-
# @!macro observable_add_observer
-
1
def add_observer(observer = nil, func = :update, &block)
-
if observer.nil? && block.nil?
-
raise ArgumentError, 'should pass observer as a first argument or block'
-
elsif observer && block
-
raise ArgumentError.new('cannot provide both an observer and a block')
-
end
-
-
if block
-
observer = block
-
func = :call
-
end
-
-
synchronize do
-
@observers[observer] = func
-
observer
-
end
-
end
-
-
# @!macro observable_delete_observer
-
1
def delete_observer(observer)
-
synchronize do
-
@observers.delete(observer)
-
observer
-
end
-
end
-
-
# @!macro observable_delete_observers
-
1
def delete_observers
-
synchronize do
-
@observers.clear
-
self
-
end
-
end
-
-
# @!macro observable_count_observers
-
1
def count_observers
-
synchronize { @observers.count }
-
end
-
-
# Notifies all registered observers with optional args
-
# @param [Object] args arguments to be passed to each observer
-
# @return [CopyOnWriteObserverSet] self
-
1
def notify_observers(*args, &block)
-
observers = duplicate_observers
-
notify_to(observers, *args, &block)
-
self
-
end
-
-
# Notifies all registered observers with optional args and deletes them.
-
#
-
# @param [Object] args arguments to be passed to each observer
-
# @return [CopyOnWriteObserverSet] self
-
1
def notify_and_delete_observers(*args, &block)
-
observers = duplicate_and_clear_observers
-
notify_to(observers, *args, &block)
-
self
-
end
-
-
1
protected
-
-
1
def ns_initialize
-
@observers = {}
-
end
-
-
1
private
-
-
1
def duplicate_and_clear_observers
-
synchronize do
-
observers = @observers.dup
-
@observers.clear
-
observers
-
end
-
end
-
-
1
def duplicate_observers
-
synchronize { @observers.dup }
-
end
-
-
1
def notify_to(observers, *args)
-
raise ArgumentError.new('cannot give arguments and a block') if block_given? && !args.empty?
-
observers.each do |observer, function|
-
args = yield if block_given?
-
observer.send(function, *args)
-
end
-
end
-
end
-
end
-
end
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
1
module Collection
-
-
# A thread safe observer set implemented using copy-on-write approach:
-
# every time an observer is added or removed the whole internal data structure is
-
# duplicated and replaced with a new one.
-
#
-
# @api private
-
1
class CopyOnWriteObserverSet < Synchronization::LockableObject
-
-
1
def initialize
-
super()
-
synchronize { ns_initialize }
-
end
-
-
# @!macro observable_add_observer
-
1
def add_observer(observer = nil, func = :update, &block)
-
if observer.nil? && block.nil?
-
raise ArgumentError, 'should pass observer as a first argument or block'
-
elsif observer && block
-
raise ArgumentError.new('cannot provide both an observer and a block')
-
end
-
-
if block
-
observer = block
-
func = :call
-
end
-
-
synchronize do
-
new_observers = @observers.dup
-
new_observers[observer] = func
-
@observers = new_observers
-
observer
-
end
-
end
-
-
# @!macro observable_delete_observer
-
1
def delete_observer(observer)
-
synchronize do
-
new_observers = @observers.dup
-
new_observers.delete(observer)
-
@observers = new_observers
-
observer
-
end
-
end
-
-
# @!macro observable_delete_observers
-
1
def delete_observers
-
self.observers = {}
-
self
-
end
-
-
# @!macro observable_count_observers
-
1
def count_observers
-
observers.count
-
end
-
-
# Notifies all registered observers with optional args
-
# @param [Object] args arguments to be passed to each observer
-
# @return [CopyOnWriteObserverSet] self
-
1
def notify_observers(*args, &block)
-
notify_to(observers, *args, &block)
-
self
-
end
-
-
# Notifies all registered observers with optional args and deletes them.
-
#
-
# @param [Object] args arguments to be passed to each observer
-
# @return [CopyOnWriteObserverSet] self
-
1
def notify_and_delete_observers(*args, &block)
-
old = clear_observers_and_return_old
-
notify_to(old, *args, &block)
-
self
-
end
-
-
1
protected
-
-
1
def ns_initialize
-
@observers = {}
-
end
-
-
1
private
-
-
1
def notify_to(observers, *args)
-
raise ArgumentError.new('cannot give arguments and a block') if block_given? && !args.empty?
-
observers.each do |observer, function|
-
args = yield if block_given?
-
observer.send(function, *args)
-
end
-
end
-
-
1
def observers
-
synchronize { @observers }
-
end
-
-
1
def observers=(new_set)
-
synchronize { @observers = new_set }
-
end
-
-
1
def clear_observers_and_return_old
-
synchronize do
-
old_observers = @observers
-
@observers = {}
-
old_observers
-
end
-
end
-
end
-
end
-
end
-
1
if Concurrent.on_jruby?
-
-
module Concurrent
-
module Collection
-
-
-
# @!macro priority_queue
-
#
-
# @!visibility private
-
# @!macro internal_implementation_note
-
class JavaNonConcurrentPriorityQueue
-
-
# @!macro priority_queue_method_initialize
-
def initialize(opts = {})
-
order = opts.fetch(:order, :max)
-
if [:min, :low].include?(order)
-
@queue = java.util.PriorityQueue.new(11) # 11 is the default initial capacity
-
else
-
@queue = java.util.PriorityQueue.new(11, java.util.Collections.reverseOrder())
-
end
-
end
-
-
# @!macro priority_queue_method_clear
-
def clear
-
@queue.clear
-
true
-
end
-
-
# @!macro priority_queue_method_delete
-
def delete(item)
-
found = false
-
while @queue.remove(item) do
-
found = true
-
end
-
found
-
end
-
-
# @!macro priority_queue_method_empty
-
def empty?
-
@queue.size == 0
-
end
-
-
# @!macro priority_queue_method_include
-
def include?(item)
-
@queue.contains(item)
-
end
-
alias_method :has_priority?, :include?
-
-
# @!macro priority_queue_method_length
-
def length
-
@queue.size
-
end
-
alias_method :size, :length
-
-
# @!macro priority_queue_method_peek
-
def peek
-
@queue.peek
-
end
-
-
# @!macro priority_queue_method_pop
-
def pop
-
@queue.poll
-
end
-
alias_method :deq, :pop
-
alias_method :shift, :pop
-
-
# @!macro priority_queue_method_push
-
def push(item)
-
raise ArgumentError.new('cannot enqueue nil') if item.nil?
-
@queue.add(item)
-
end
-
alias_method :<<, :push
-
alias_method :enq, :push
-
-
# @!macro priority_queue_method_from_list
-
def self.from_list(list, opts = {})
-
queue = new(opts)
-
list.each{|item| queue << item }
-
queue
-
end
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
-
# @!macro warn.edge
-
1
class LockFreeStack < Synchronization::Object
-
-
1
safe_initialization!
-
-
1
class Node
-
# TODO (pitr-ch 20-Dec-2016): Could be unified with Stack class?
-
-
# @return [Node]
-
1
attr_reader :next_node
-
-
# @return [Object]
-
1
attr_reader :value
-
-
# @!visibility private
-
# allow to nil-ify to free GC when the entry is no longer relevant, not synchronised
-
1
attr_writer :value
-
-
1
def initialize(value, next_node)
-
1
@value = value
-
1
@next_node = next_node
-
end
-
-
1
singleton_class.send :alias_method, :[], :new
-
end
-
-
# The singleton for empty node
-
1
EMPTY = Node[nil, nil]
-
1
def EMPTY.next_node
-
self
-
end
-
-
1
attr_atomic(:head)
-
1
private :head, :head=, :swap_head, :compare_and_set_head, :update_head
-
-
# @!visibility private
-
1
def self.of1(value)
-
new Node[value, EMPTY]
-
end
-
-
# @!visibility private
-
1
def self.of2(value1, value2)
-
new Node[value1, Node[value2, EMPTY]]
-
end
-
-
# @param [Node] head
-
1
def initialize(head = EMPTY)
-
super()
-
self.head = head
-
end
-
-
# @param [Node] head
-
# @return [true, false]
-
1
def empty?(head = head())
-
head.equal? EMPTY
-
end
-
-
# @param [Node] head
-
# @param [Object] value
-
# @return [true, false]
-
1
def compare_and_push(head, value)
-
compare_and_set_head head, Node[value, head]
-
end
-
-
# @param [Object] value
-
# @return [self]
-
1
def push(value)
-
while true
-
current_head = head
-
return self if compare_and_set_head current_head, Node[value, current_head]
-
end
-
end
-
-
# @return [Node]
-
1
def peek
-
head
-
end
-
-
# @param [Node] head
-
# @return [true, false]
-
1
def compare_and_pop(head)
-
compare_and_set_head head, head.next_node
-
end
-
-
# @return [Object]
-
1
def pop
-
while true
-
current_head = head
-
return current_head.value if compare_and_set_head current_head, current_head.next_node
-
end
-
end
-
-
# @param [Node] head
-
# @return [true, false]
-
1
def compare_and_clear(head)
-
compare_and_set_head head, EMPTY
-
end
-
-
1
include Enumerable
-
-
# @param [Node] head
-
# @return [self]
-
1
def each(head = nil)
-
return to_enum(:each, head) unless block_given?
-
it = head || peek
-
until it.equal?(EMPTY)
-
yield it.value
-
it = it.next_node
-
end
-
self
-
end
-
-
# @return [true, false]
-
1
def clear
-
while true
-
current_head = head
-
return false if current_head == EMPTY
-
return true if compare_and_set_head current_head, EMPTY
-
end
-
end
-
-
# @param [Node] head
-
# @return [true, false]
-
1
def clear_if(head)
-
compare_and_set_head head, EMPTY
-
end
-
-
# @param [Node] head
-
# @param [Node] new_head
-
# @return [true, false]
-
1
def replace_if(head, new_head)
-
compare_and_set_head head, new_head
-
end
-
-
# @return [self]
-
# @yield over the cleared stack
-
# @yieldparam [Object] value
-
1
def clear_each(&block)
-
while true
-
current_head = head
-
return self if current_head == EMPTY
-
if compare_and_set_head current_head, EMPTY
-
each current_head, &block
-
return self
-
end
-
end
-
end
-
-
# @return [String] Short string representation.
-
1
def to_s
-
format '%s %s>', super[0..-2], to_a.to_s
-
end
-
-
1
alias_method :inspect, :to_s
-
end
-
end
-
1
require 'thread'
-
1
require 'concurrent/collection/map/non_concurrent_map_backend'
-
-
1
module Concurrent
-
-
# @!visibility private
-
1
module Collection
-
-
# @!visibility private
-
1
class MriMapBackend < NonConcurrentMapBackend
-
-
1
def initialize(options = nil)
-
super(options)
-
@write_lock = Mutex.new
-
end
-
-
1
def []=(key, value)
-
@write_lock.synchronize { super }
-
end
-
-
1
def compute_if_absent(key)
-
if stored_value = _get(key) # fast non-blocking path for the most likely case
-
stored_value
-
else
-
@write_lock.synchronize { super }
-
end
-
end
-
-
1
def compute_if_present(key)
-
@write_lock.synchronize { super }
-
end
-
-
1
def compute(key)
-
@write_lock.synchronize { super }
-
end
-
-
1
def merge_pair(key, value)
-
@write_lock.synchronize { super }
-
end
-
-
1
def replace_pair(key, old_value, new_value)
-
@write_lock.synchronize { super }
-
end
-
-
1
def replace_if_exists(key, new_value)
-
@write_lock.synchronize { super }
-
end
-
-
1
def get_and_set(key, value)
-
@write_lock.synchronize { super }
-
end
-
-
1
def delete(key)
-
@write_lock.synchronize { super }
-
end
-
-
1
def delete_pair(key, value)
-
@write_lock.synchronize { super }
-
end
-
-
1
def clear
-
@write_lock.synchronize { super }
-
end
-
end
-
end
-
end
-
1
require 'concurrent/constants'
-
-
1
module Concurrent
-
-
# @!visibility private
-
1
module Collection
-
-
# @!visibility private
-
1
class NonConcurrentMapBackend
-
-
# WARNING: all public methods of the class must operate on the @backend
-
# directly without calling each other. This is important because of the
-
# SynchronizedMapBackend which uses a non-reentrant mutex for performance
-
# reasons.
-
1
def initialize(options = nil)
-
@backend = {}
-
end
-
-
1
def [](key)
-
@backend[key]
-
end
-
-
1
def []=(key, value)
-
@backend[key] = value
-
end
-
-
1
def compute_if_absent(key)
-
if NULL != (stored_value = @backend.fetch(key, NULL))
-
stored_value
-
else
-
@backend[key] = yield
-
end
-
end
-
-
1
def replace_pair(key, old_value, new_value)
-
if pair?(key, old_value)
-
@backend[key] = new_value
-
true
-
else
-
false
-
end
-
end
-
-
1
def replace_if_exists(key, new_value)
-
if NULL != (stored_value = @backend.fetch(key, NULL))
-
@backend[key] = new_value
-
stored_value
-
end
-
end
-
-
1
def compute_if_present(key)
-
if NULL != (stored_value = @backend.fetch(key, NULL))
-
store_computed_value(key, yield(stored_value))
-
end
-
end
-
-
1
def compute(key)
-
store_computed_value(key, yield(@backend[key]))
-
end
-
-
1
def merge_pair(key, value)
-
if NULL == (stored_value = @backend.fetch(key, NULL))
-
@backend[key] = value
-
else
-
store_computed_value(key, yield(stored_value))
-
end
-
end
-
-
1
def get_and_set(key, value)
-
stored_value = @backend[key]
-
@backend[key] = value
-
stored_value
-
end
-
-
1
def key?(key)
-
@backend.key?(key)
-
end
-
-
1
def delete(key)
-
@backend.delete(key)
-
end
-
-
1
def delete_pair(key, value)
-
if pair?(key, value)
-
@backend.delete(key)
-
true
-
else
-
false
-
end
-
end
-
-
1
def clear
-
@backend.clear
-
self
-
end
-
-
1
def each_pair
-
dupped_backend.each_pair do |k, v|
-
yield k, v
-
end
-
self
-
end
-
-
1
def size
-
@backend.size
-
end
-
-
1
def get_or_default(key, default_value)
-
@backend.fetch(key, default_value)
-
end
-
-
1
alias_method :_get, :[]
-
1
alias_method :_set, :[]=
-
1
private :_get, :_set
-
1
private
-
1
def initialize_copy(other)
-
super
-
@backend = {}
-
self
-
end
-
-
1
def dupped_backend
-
@backend.dup
-
end
-
-
1
def pair?(key, expected_value)
-
NULL != (stored_value = @backend.fetch(key, NULL)) && expected_value.equal?(stored_value)
-
end
-
-
1
def store_computed_value(key, new_value)
-
if new_value.nil?
-
@backend.delete(key)
-
nil
-
else
-
@backend[key] = new_value
-
end
-
end
-
end
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/collection/java_non_concurrent_priority_queue'
-
1
require 'concurrent/collection/ruby_non_concurrent_priority_queue'
-
-
1
module Concurrent
-
1
module Collection
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
NonConcurrentPriorityQueueImplementation = case
-
1
when Concurrent.on_jruby?
-
JavaNonConcurrentPriorityQueue
-
else
-
1
RubyNonConcurrentPriorityQueue
-
end
-
1
private_constant :NonConcurrentPriorityQueueImplementation
-
-
# @!macro priority_queue
-
#
-
# A queue collection in which the elements are sorted based on their
-
# comparison (spaceship) operator `<=>`. Items are added to the queue
-
# at a position relative to their priority. On removal the element
-
# with the "highest" priority is removed. By default the sort order is
-
# from highest to lowest, but a lowest-to-highest sort order can be
-
# set on construction.
-
#
-
# The API is based on the `Queue` class from the Ruby standard library.
-
#
-
# The pure Ruby implementation, `RubyNonConcurrentPriorityQueue` uses a heap algorithm
-
# stored in an array. The algorithm is based on the work of Robert Sedgewick
-
# and Kevin Wayne.
-
#
-
# The JRuby native implementation is a thin wrapper around the standard
-
# library `java.util.NonConcurrentPriorityQueue`.
-
#
-
# When running under JRuby the class `NonConcurrentPriorityQueue` extends `JavaNonConcurrentPriorityQueue`.
-
# When running under all other interpreters it extends `RubyNonConcurrentPriorityQueue`.
-
#
-
# @note This implementation is *not* thread safe.
-
#
-
# @see http://en.wikipedia.org/wiki/Priority_queue
-
# @see http://ruby-doc.org/stdlib-2.0.0/libdoc/thread/rdoc/Queue.html
-
#
-
# @see http://algs4.cs.princeton.edu/24pq/index.php#2.6
-
# @see http://algs4.cs.princeton.edu/24pq/MaxPQ.java.html
-
#
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/PriorityQueue.html
-
#
-
# @!visibility private
-
1
class NonConcurrentPriorityQueue < NonConcurrentPriorityQueueImplementation
-
-
1
alias_method :has_priority?, :include?
-
-
1
alias_method :size, :length
-
-
1
alias_method :deq, :pop
-
1
alias_method :shift, :pop
-
-
1
alias_method :<<, :push
-
1
alias_method :enq, :push
-
-
# @!method initialize(opts = {})
-
# @!macro priority_queue_method_initialize
-
#
-
# Create a new priority queue with no items.
-
#
-
# @param [Hash] opts the options for creating the queue
-
# @option opts [Symbol] :order (:max) dictates the order in which items are
-
# stored: from highest to lowest when `:max` or `:high`; from lowest to
-
# highest when `:min` or `:low`
-
-
# @!method clear
-
# @!macro priority_queue_method_clear
-
#
-
# Removes all of the elements from this priority queue.
-
-
# @!method delete(item)
-
# @!macro priority_queue_method_delete
-
#
-
# Deletes all items from `self` that are equal to `item`.
-
#
-
# @param [Object] item the item to be removed from the queue
-
# @return [Object] true if the item is found else false
-
-
# @!method empty?
-
# @!macro priority_queue_method_empty
-
#
-
# Returns `true` if `self` contains no elements.
-
#
-
# @return [Boolean] true if there are no items in the queue else false
-
-
# @!method include?(item)
-
# @!macro priority_queue_method_include
-
#
-
# Returns `true` if the given item is present in `self` (that is, if any
-
# element == `item`), otherwise returns false.
-
#
-
# @param [Object] item the item to search for
-
#
-
# @return [Boolean] true if the item is found else false
-
-
# @!method length
-
# @!macro priority_queue_method_length
-
#
-
# The current length of the queue.
-
#
-
# @return [Fixnum] the number of items in the queue
-
-
# @!method peek
-
# @!macro priority_queue_method_peek
-
#
-
# Retrieves, but does not remove, the head of this queue, or returns `nil`
-
# if this queue is empty.
-
#
-
# @return [Object] the head of the queue or `nil` when empty
-
-
# @!method pop
-
# @!macro priority_queue_method_pop
-
#
-
# Retrieves and removes the head of this queue, or returns `nil` if this
-
# queue is empty.
-
#
-
# @return [Object] the head of the queue or `nil` when empty
-
-
# @!method push(item)
-
# @!macro priority_queue_method_push
-
#
-
# Inserts the specified element into this priority queue.
-
#
-
# @param [Object] item the item to insert onto the queue
-
-
# @!method self.from_list(list, opts = {})
-
# @!macro priority_queue_method_from_list
-
#
-
# Create a new priority queue from the given list.
-
#
-
# @param [Enumerable] list the list to build the queue from
-
# @param [Hash] opts the options for creating the queue
-
#
-
# @return [NonConcurrentPriorityQueue] the newly created and populated queue
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Collection
-
-
# @!macro priority_queue
-
#
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class RubyNonConcurrentPriorityQueue
-
-
# @!macro priority_queue_method_initialize
-
1
def initialize(opts = {})
-
order = opts.fetch(:order, :max)
-
@comparator = [:min, :low].include?(order) ? -1 : 1
-
clear
-
end
-
-
# @!macro priority_queue_method_clear
-
1
def clear
-
@queue = [nil]
-
@length = 0
-
true
-
end
-
-
# @!macro priority_queue_method_delete
-
1
def delete(item)
-
return false if empty?
-
original_length = @length
-
k = 1
-
while k <= @length
-
if @queue[k] == item
-
swap(k, @length)
-
@length -= 1
-
sink(k)
-
@queue.pop
-
else
-
k += 1
-
end
-
end
-
@length != original_length
-
end
-
-
# @!macro priority_queue_method_empty
-
1
def empty?
-
size == 0
-
end
-
-
# @!macro priority_queue_method_include
-
1
def include?(item)
-
@queue.include?(item)
-
end
-
1
alias_method :has_priority?, :include?
-
-
# @!macro priority_queue_method_length
-
1
def length
-
@length
-
end
-
1
alias_method :size, :length
-
-
# @!macro priority_queue_method_peek
-
1
def peek
-
empty? ? nil : @queue[1]
-
end
-
-
# @!macro priority_queue_method_pop
-
1
def pop
-
return nil if empty?
-
max = @queue[1]
-
swap(1, @length)
-
@length -= 1
-
sink(1)
-
@queue.pop
-
max
-
end
-
1
alias_method :deq, :pop
-
1
alias_method :shift, :pop
-
-
# @!macro priority_queue_method_push
-
1
def push(item)
-
raise ArgumentError.new('cannot enqueue nil') if item.nil?
-
@length += 1
-
@queue << item
-
swim(@length)
-
true
-
end
-
1
alias_method :<<, :push
-
1
alias_method :enq, :push
-
-
# @!macro priority_queue_method_from_list
-
1
def self.from_list(list, opts = {})
-
queue = new(opts)
-
list.each{|item| queue << item }
-
queue
-
end
-
-
1
private
-
-
# Exchange the values at the given indexes within the internal array.
-
#
-
# @param [Integer] x the first index to swap
-
# @param [Integer] y the second index to swap
-
#
-
# @!visibility private
-
1
def swap(x, y)
-
temp = @queue[x]
-
@queue[x] = @queue[y]
-
@queue[y] = temp
-
end
-
-
# Are the items at the given indexes ordered based on the priority
-
# order specified at construction?
-
#
-
# @param [Integer] x the first index from which to retrieve a comparable value
-
# @param [Integer] y the second index from which to retrieve a comparable value
-
#
-
# @return [Boolean] true if the two elements are in the correct priority order
-
# else false
-
#
-
# @!visibility private
-
1
def ordered?(x, y)
-
(@queue[x] <=> @queue[y]) == @comparator
-
end
-
-
# Percolate down to maintain heap invariant.
-
#
-
# @param [Integer] k the index at which to start the percolation
-
#
-
# @!visibility private
-
1
def sink(k)
-
while (j = (2 * k)) <= @length do
-
j += 1 if j < @length && ! ordered?(j, j+1)
-
break if ordered?(k, j)
-
swap(k, j)
-
k = j
-
end
-
end
-
-
# Percolate up to maintain heap invariant.
-
#
-
# @param [Integer] k the index at which to start the percolation
-
#
-
# @!visibility private
-
1
def swim(k)
-
while k > 1 && ! ordered?(k/2, k) do
-
swap(k, k/2)
-
k = k/2
-
end
-
end
-
end
-
end
-
end
-
1
require 'concurrent/concern/logging'
-
-
1
module Concurrent
-
1
module Concern
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
module Deprecation
-
# TODO require additional parameter: a version. Display when it'll be removed based on that. Error if not removed.
-
1
include Concern::Logging
-
-
1
def deprecated(message, strip = 2)
-
caller_line = caller(strip).first if strip > 0
-
klass = if Module === self
-
self
-
else
-
self.class
-
end
-
message = if strip > 0
-
format("[DEPRECATED] %s\ncalled on: %s", message, caller_line)
-
else
-
format('[DEPRECATED] %s', message)
-
end
-
log WARN, klass.to_s, message
-
end
-
-
1
def deprecated_method(old_name, new_name)
-
deprecated "`#{old_name}` is deprecated and it'll removed in next release, use `#{new_name}` instead", 3
-
end
-
-
1
extend self
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Concern
-
-
# Object references in Ruby are mutable. This can lead to serious problems when
-
# the `#value` of a concurrent object is a mutable reference. Which is always the
-
# case unless the value is a `Fixnum`, `Symbol`, or similar "primitive" data type.
-
# Most classes in this library that expose a `#value` getter method do so using the
-
# `Dereferenceable` mixin module.
-
#
-
# @!macro copy_options
-
1
module Dereferenceable
-
# NOTE: This module is going away in 2.0. In the mean time we need it to
-
# play nicely with the synchronization layer. This means that the
-
# including class SHOULD be synchronized and it MUST implement a
-
# `#synchronize` method. Not doing so will lead to runtime errors.
-
-
# Return the value this object represents after applying the options specified
-
# by the `#set_deref_options` method.
-
#
-
# @return [Object] the current value of the object
-
1
def value
-
synchronize { apply_deref_options(@value) }
-
end
-
1
alias_method :deref, :value
-
-
1
protected
-
-
# Set the internal value of this object
-
#
-
# @param [Object] value the new value
-
1
def value=(value)
-
synchronize{ @value = value }
-
end
-
-
# @!macro dereferenceable_set_deref_options
-
# Set the options which define the operations #value performs before
-
# returning data to the caller (dereferencing).
-
#
-
# @note Most classes that include this module will call `#set_deref_options`
-
# from within the constructor, thus allowing these options to be set at
-
# object creation.
-
#
-
# @param [Hash] opts the options defining dereference behavior.
-
# @option opts [String] :dup_on_deref (false) call `#dup` before returning the data
-
# @option opts [String] :freeze_on_deref (false) call `#freeze` before returning the data
-
# @option opts [String] :copy_on_deref (nil) call the given `Proc` passing
-
# the internal value and returning the value returned from the proc
-
1
def set_deref_options(opts = {})
-
10
synchronize{ ns_set_deref_options(opts) }
-
end
-
-
# @!macro dereferenceable_set_deref_options
-
# @!visibility private
-
1
def ns_set_deref_options(opts)
-
5
@dup_on_deref = opts[:dup_on_deref] || opts[:dup]
-
5
@freeze_on_deref = opts[:freeze_on_deref] || opts[:freeze]
-
5
@copy_on_deref = opts[:copy_on_deref] || opts[:copy]
-
5
@do_nothing_on_deref = !(@dup_on_deref || @freeze_on_deref || @copy_on_deref)
-
nil
-
end
-
-
# @!visibility private
-
1
def apply_deref_options(value)
-
return nil if value.nil?
-
return value if @do_nothing_on_deref
-
value = @copy_on_deref.call(value) if @copy_on_deref
-
value = value.dup if @dup_on_deref
-
value = value.freeze if @freeze_on_deref
-
value
-
end
-
end
-
end
-
end
-
1
require 'logger'
-
-
1
module Concurrent
-
1
module Concern
-
-
# Include where logging is needed
-
#
-
# @!visibility private
-
1
module Logging
-
1
include Logger::Severity
-
-
# Logs through {Concurrent.global_logger}, it can be overridden by setting @logger
-
# @param [Integer] level one of Logger::Severity constants
-
# @param [String] progname e.g. a path of an Actor
-
# @param [String, nil] message when nil block is used to generate the message
-
# @yieldreturn [String] a message
-
1
def log(level, progname, message = nil, &block)
-
#NOTE: Cannot require 'concurrent/configuration' above due to circular references.
-
# Assume that the gem has been initialized if we've gotten this far.
-
logger = if defined?(@logger) && @logger
-
@logger
-
else
-
Concurrent.global_logger
-
end
-
logger.call level, progname, message, &block
-
rescue => error
-
$stderr.puts "`Concurrent.configuration.logger` failed to log #{[level, progname, message, block]}\n" +
-
"#{error.message} (#{error.class})\n#{error.backtrace.join "\n"}"
-
end
-
end
-
end
-
end
-
1
require 'concurrent/collection/copy_on_notify_observer_set'
-
1
require 'concurrent/collection/copy_on_write_observer_set'
-
-
1
module Concurrent
-
1
module Concern
-
-
# The [observer pattern](http://en.wikipedia.org/wiki/Observer_pattern) is one
-
# of the most useful design patterns.
-
#
-
# The workflow is very simple:
-
# - an `observer` can register itself to a `subject` via a callback
-
# - many `observers` can be registered to the same `subject`
-
# - the `subject` notifies all registered observers when its status changes
-
# - an `observer` can deregister itself when is no more interested to receive
-
# event notifications
-
#
-
# In a single threaded environment the whole pattern is very easy: the
-
# `subject` can use a simple data structure to manage all its subscribed
-
# `observer`s and every `observer` can react directly to every event without
-
# caring about synchronization.
-
#
-
# In a multi threaded environment things are more complex. The `subject` must
-
# synchronize the access to its data structure and to do so currently we're
-
# using two specialized ObserverSet: {Concurrent::Concern::CopyOnWriteObserverSet}
-
# and {Concurrent::Concern::CopyOnNotifyObserverSet}.
-
#
-
# When implementing and `observer` there's a very important rule to remember:
-
# **there are no guarantees about the thread that will execute the callback**
-
#
-
# Let's take this example
-
# ```
-
# class Observer
-
# def initialize
-
# @count = 0
-
# end
-
#
-
# def update
-
# @count += 1
-
# end
-
# end
-
#
-
# obs = Observer.new
-
# [obj1, obj2, obj3, obj4].each { |o| o.add_observer(obs) }
-
# # execute [obj1, obj2, obj3, obj4]
-
# ```
-
#
-
# `obs` is wrong because the variable `@count` can be accessed by different
-
# threads at the same time, so it should be synchronized (using either a Mutex
-
# or an AtomicFixum)
-
1
module Observable
-
-
# @!macro observable_add_observer
-
#
-
# Adds an observer to this set. If a block is passed, the observer will be
-
# created by this method and no other params should be passed.
-
#
-
# @param [Object] observer the observer to add
-
# @param [Symbol] func the function to call on the observer during notification.
-
# Default is :update
-
# @return [Object] the added observer
-
1
def add_observer(observer = nil, func = :update, &block)
-
observers.add_observer(observer, func, &block)
-
end
-
-
# As `#add_observer` but can be used for chaining.
-
#
-
# @param [Object] observer the observer to add
-
# @param [Symbol] func the function to call on the observer during notification.
-
# @return [Observable] self
-
1
def with_observer(observer = nil, func = :update, &block)
-
add_observer(observer, func, &block)
-
self
-
end
-
-
# @!macro observable_delete_observer
-
#
-
# Remove `observer` as an observer on this object so that it will no
-
# longer receive notifications.
-
#
-
# @param [Object] observer the observer to remove
-
# @return [Object] the deleted observer
-
1
def delete_observer(observer)
-
observers.delete_observer(observer)
-
end
-
-
# @!macro observable_delete_observers
-
#
-
# Remove all observers associated with this object.
-
#
-
# @return [Observable] self
-
1
def delete_observers
-
observers.delete_observers
-
self
-
end
-
-
# @!macro observable_count_observers
-
#
-
# Return the number of observers associated with this object.
-
#
-
# @return [Integer] the observers count
-
1
def count_observers
-
observers.count_observers
-
end
-
-
1
protected
-
-
1
attr_accessor :observers
-
end
-
end
-
end
-
1
require 'thread'
-
1
require 'concurrent/delay'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/atomic/atomic_reference'
-
1
require 'concurrent/concern/logging'
-
1
require 'concurrent/concern/deprecation'
-
1
require 'concurrent/executor/immediate_executor'
-
1
require 'concurrent/executor/cached_thread_pool'
-
1
require 'concurrent/utility/processor_counter'
-
-
1
module Concurrent
-
1
extend Concern::Logging
-
1
extend Concern::Deprecation
-
-
1
autoload :Options, 'concurrent/options'
-
1
autoload :TimerSet, 'concurrent/executor/timer_set'
-
1
autoload :ThreadPoolExecutor, 'concurrent/executor/thread_pool_executor'
-
-
# @return [Logger] Logger with provided level and output.
-
1
def self.create_simple_logger(level = Logger::FATAL, output = $stderr)
-
# TODO (pitr-ch 24-Dec-2016): figure out why it had to be replaced, stdlogger was deadlocking
-
1
lambda do |severity, progname, message = nil, &block|
-
return false if severity < level
-
-
message = block ? block.call : message
-
formatted_message = case message
-
when String
-
message
-
when Exception
-
format "%s (%s)\n%s",
-
message.message, message.class, (message.backtrace || []).join("\n")
-
else
-
message.inspect
-
end
-
-
output.print format "[%s] %5s -- %s: %s\n",
-
Time.now.strftime('%Y-%m-%d %H:%M:%S.%L'),
-
Logger::SEV_LABEL[severity],
-
progname,
-
formatted_message
-
true
-
end
-
end
-
-
# Use logger created by #create_simple_logger to log concurrent-ruby messages.
-
1
def self.use_simple_logger(level = Logger::FATAL, output = $stderr)
-
Concurrent.global_logger = create_simple_logger level, output
-
end
-
-
# @return [Logger] Logger with provided level and output.
-
# @deprecated
-
1
def self.create_stdlib_logger(level = Logger::FATAL, output = $stderr)
-
logger = Logger.new(output)
-
logger.level = level
-
logger.formatter = lambda do |severity, datetime, progname, msg|
-
formatted_message = case msg
-
when String
-
msg
-
when Exception
-
format "%s (%s)\n%s",
-
msg.message, msg.class, (msg.backtrace || []).join("\n")
-
else
-
msg.inspect
-
end
-
format "[%s] %5s -- %s: %s\n",
-
datetime.strftime('%Y-%m-%d %H:%M:%S.%L'),
-
severity,
-
progname,
-
formatted_message
-
end
-
-
lambda do |loglevel, progname, message = nil, &block|
-
logger.add loglevel, message, progname, &block
-
end
-
end
-
-
# Use logger created by #create_stdlib_logger to log concurrent-ruby messages.
-
# @deprecated
-
1
def self.use_stdlib_logger(level = Logger::FATAL, output = $stderr)
-
Concurrent.global_logger = create_stdlib_logger level, output
-
end
-
-
# TODO (pitr-ch 27-Dec-2016): remove deadlocking stdlib_logger methods
-
-
# Suppresses all output when used for logging.
-
1
NULL_LOGGER = lambda { |level, progname, message = nil, &block| }
-
-
# @!visibility private
-
1
GLOBAL_LOGGER = AtomicReference.new(create_simple_logger(Logger::WARN))
-
1
private_constant :GLOBAL_LOGGER
-
-
1
def self.global_logger
-
GLOBAL_LOGGER.value
-
end
-
-
1
def self.global_logger=(value)
-
GLOBAL_LOGGER.value = value
-
end
-
-
# @!visibility private
-
1
GLOBAL_FAST_EXECUTOR = Delay.new { Concurrent.new_fast_executor }
-
1
private_constant :GLOBAL_FAST_EXECUTOR
-
-
# @!visibility private
-
1
GLOBAL_IO_EXECUTOR = Delay.new { Concurrent.new_io_executor }
-
1
private_constant :GLOBAL_IO_EXECUTOR
-
-
# @!visibility private
-
1
GLOBAL_TIMER_SET = Delay.new { TimerSet.new }
-
1
private_constant :GLOBAL_TIMER_SET
-
-
# @!visibility private
-
1
GLOBAL_IMMEDIATE_EXECUTOR = ImmediateExecutor.new
-
1
private_constant :GLOBAL_IMMEDIATE_EXECUTOR
-
-
# Disables AtExit handlers including pool auto-termination handlers.
-
# When disabled it will be the application programmer's responsibility
-
# to ensure that the handlers are shutdown properly prior to application
-
# exit by calling `AtExit.run` method.
-
#
-
# @note this option should be needed only because of `at_exit` ordering
-
# issues which may arise when running some of the testing frameworks.
-
# E.g. Minitest's test-suite runs itself in `at_exit` callback which
-
# executes after the pools are already terminated. Then auto termination
-
# needs to be disabled and called manually after test-suite ends.
-
# @note This method should *never* be called
-
# from within a gem. It should *only* be used from within the main
-
# application and even then it should be used only when necessary.
-
# @deprecated Has no effect since it is no longer needed, see https://github.com/ruby-concurrency/concurrent-ruby/pull/841.
-
#
-
1
def self.disable_at_exit_handlers!
-
deprecated "Method #disable_at_exit_handlers! has no effect since it is no longer needed, see https://github.com/ruby-concurrency/concurrent-ruby/pull/841."
-
end
-
-
# Global thread pool optimized for short, fast *operations*.
-
#
-
# @return [ThreadPoolExecutor] the thread pool
-
1
def self.global_fast_executor
-
GLOBAL_FAST_EXECUTOR.value
-
end
-
-
# Global thread pool optimized for long, blocking (IO) *tasks*.
-
#
-
# @return [ThreadPoolExecutor] the thread pool
-
1
def self.global_io_executor
-
GLOBAL_IO_EXECUTOR.value
-
end
-
-
1
def self.global_immediate_executor
-
GLOBAL_IMMEDIATE_EXECUTOR
-
end
-
-
# Global thread pool user for global *timers*.
-
#
-
# @return [Concurrent::TimerSet] the thread pool
-
1
def self.global_timer_set
-
GLOBAL_TIMER_SET.value
-
end
-
-
# General access point to global executors.
-
# @param [Symbol, Executor] executor_identifier symbols:
-
# - :fast - {Concurrent.global_fast_executor}
-
# - :io - {Concurrent.global_io_executor}
-
# - :immediate - {Concurrent.global_immediate_executor}
-
# @return [Executor]
-
1
def self.executor(executor_identifier)
-
Options.executor(executor_identifier)
-
end
-
-
1
def self.new_fast_executor(opts = {})
-
FixedThreadPool.new(
-
[2, Concurrent.processor_count].max,
-
auto_terminate: opts.fetch(:auto_terminate, true),
-
idletime: 60, # 1 minute
-
max_queue: 0, # unlimited
-
fallback_policy: :abort, # shouldn't matter -- 0 max queue
-
name: "fast"
-
)
-
end
-
-
1
def self.new_io_executor(opts = {})
-
CachedThreadPool.new(
-
auto_terminate: opts.fetch(:auto_terminate, true),
-
fallback_policy: :abort, # shouldn't matter -- 0 max queue
-
name: "io"
-
)
-
end
-
end
-
1
module Concurrent
-
-
# Various classes within allows for +nil+ values to be stored,
-
# so a special +NULL+ token is required to indicate the "nil-ness".
-
# @!visibility private
-
1
NULL = ::Object.new
-
-
end
-
1
require 'concurrent/future'
-
1
require 'concurrent/atomic/atomic_fixnum'
-
-
1
module Concurrent
-
-
# @!visibility private
-
1
class DependencyCounter # :nodoc:
-
-
1
def initialize(count, &block)
-
@counter = AtomicFixnum.new(count)
-
@block = block
-
end
-
-
1
def update(time, value, reason)
-
if @counter.decrement == 0
-
@block.call
-
end
-
end
-
end
-
-
# Dataflow allows you to create a task that will be scheduled when all of its data dependencies are available.
-
# {include:file:docs-source/dataflow.md}
-
#
-
# @param [Future] inputs zero or more `Future` operations that this dataflow depends upon
-
#
-
# @yield The operation to perform once all the dependencies are met
-
# @yieldparam [Future] inputs each of the `Future` inputs to the dataflow
-
# @yieldreturn [Object] the result of the block operation
-
#
-
# @return [Object] the result of all the operations
-
#
-
# @raise [ArgumentError] if no block is given
-
# @raise [ArgumentError] if any of the inputs are not `IVar`s
-
1
def dataflow(*inputs, &block)
-
dataflow_with(Concurrent.global_io_executor, *inputs, &block)
-
end
-
1
module_function :dataflow
-
-
1
def dataflow_with(executor, *inputs, &block)
-
call_dataflow(:value, executor, *inputs, &block)
-
end
-
1
module_function :dataflow_with
-
-
1
def dataflow!(*inputs, &block)
-
dataflow_with!(Concurrent.global_io_executor, *inputs, &block)
-
end
-
1
module_function :dataflow!
-
-
1
def dataflow_with!(executor, *inputs, &block)
-
call_dataflow(:value!, executor, *inputs, &block)
-
end
-
1
module_function :dataflow_with!
-
-
1
private
-
-
1
def call_dataflow(method, executor, *inputs, &block)
-
raise ArgumentError.new('an executor must be provided') if executor.nil?
-
raise ArgumentError.new('no block given') unless block_given?
-
unless inputs.all? { |input| input.is_a? IVar }
-
raise ArgumentError.new("Not all dependencies are IVars.\nDependencies: #{ inputs.inspect }")
-
end
-
-
result = Future.new(executor: executor) do
-
values = inputs.map { |input| input.send(method) }
-
block.call(*values)
-
end
-
-
if inputs.empty?
-
result.execute
-
else
-
counter = DependencyCounter.new(inputs.size) { result.execute }
-
-
inputs.each do |input|
-
input.add_observer counter
-
end
-
end
-
-
result
-
end
-
1
module_function :call_dataflow
-
end
-
1
require 'thread'
-
1
require 'concurrent/concern/obligation'
-
1
require 'concurrent/executor/immediate_executor'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# This file has circular require issues. It must be autoloaded here.
-
1
autoload :Options, 'concurrent/options'
-
-
# Lazy evaluation of a block yielding an immutable result. Useful for
-
# expensive operations that may never be needed. It may be non-blocking,
-
# supports the `Concern::Obligation` interface, and accepts the injection of
-
# custom executor upon which to execute the block. Processing of
-
# block will be deferred until the first time `#value` is called.
-
# At that time the caller can choose to return immediately and let
-
# the block execute asynchronously, block indefinitely, or block
-
# with a timeout.
-
#
-
# When a `Delay` is created its state is set to `pending`. The value and
-
# reason are both `nil`. The first time the `#value` method is called the
-
# enclosed opration will be run and the calling thread will block. Other
-
# threads attempting to call `#value` will block as well. Once the operation
-
# is complete the *value* will be set to the result of the operation or the
-
# *reason* will be set to the raised exception, as appropriate. All threads
-
# blocked on `#value` will return. Subsequent calls to `#value` will immediately
-
# return the cached value. The operation will only be run once. This means that
-
# any side effects created by the operation will only happen once as well.
-
#
-
# `Delay` includes the `Concurrent::Concern::Dereferenceable` mixin to support thread
-
# safety of the reference returned by `#value`.
-
#
-
# @!macro copy_options
-
#
-
# @!macro delay_note_regarding_blocking
-
# @note The default behavior of `Delay` is to block indefinitely when
-
# calling either `value` or `wait`, executing the delayed operation on
-
# the current thread. This makes the `timeout` value completely
-
# irrelevant. To enable non-blocking behavior, use the `executor`
-
# constructor option. This will cause the delayed operation to be
-
# execute on the given executor, allowing the call to timeout.
-
#
-
# @see Concurrent::Concern::Dereferenceable
-
1
class Delay < Synchronization::LockableObject
-
1
include Concern::Obligation
-
-
# NOTE: Because the global thread pools are lazy-loaded with these objects
-
# there is a performance hit every time we post a new task to one of these
-
# thread pools. Subsequently it is critical that `Delay` perform as fast
-
# as possible post-completion. This class has been highly optimized using
-
# the benchmark script `examples/lazy_and_delay.rb`. Do NOT attempt to
-
# DRY-up this class or perform other refactoring with running the
-
# benchmarks and ensuring that performance is not negatively impacted.
-
-
# Create a new `Delay` in the `:pending` state.
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @yield the delayed operation to perform
-
#
-
# @raise [ArgumentError] if no block is given
-
1
def initialize(opts = {}, &block)
-
5
raise ArgumentError.new('no block given') unless block_given?
-
5
super(&nil)
-
10
synchronize { ns_initialize(opts, &block) }
-
end
-
-
# Return the value this object represents after applying the options
-
# specified by the `#set_deref_options` method. If the delayed operation
-
# raised an exception this method will return nil. The execption object
-
# can be accessed via the `#reason` method.
-
#
-
# @param [Numeric] timeout the maximum number of seconds to wait
-
# @return [Object] the current value of the object
-
#
-
# @!macro delay_note_regarding_blocking
-
1
def value(timeout = nil)
-
if @executor # TODO (pitr 12-Sep-2015): broken unsafe read?
-
super
-
else
-
# this function has been optimized for performance and
-
# should not be modified without running new benchmarks
-
synchronize do
-
execute = @evaluation_started = true unless @evaluation_started
-
if execute
-
begin
-
set_state(true, @task.call, nil)
-
rescue => ex
-
set_state(false, nil, ex)
-
end
-
elsif incomplete?
-
raise IllegalOperationError, 'Recursive call to #value during evaluation of the Delay'
-
end
-
end
-
if @do_nothing_on_deref
-
@value
-
else
-
apply_deref_options(@value)
-
end
-
end
-
end
-
-
# Return the value this object represents after applying the options
-
# specified by the `#set_deref_options` method. If the delayed operation
-
# raised an exception, this method will raise that exception (even when)
-
# the operation has already been executed).
-
#
-
# @param [Numeric] timeout the maximum number of seconds to wait
-
# @return [Object] the current value of the object
-
# @raise [Exception] when `#rejected?` raises `#reason`
-
#
-
# @!macro delay_note_regarding_blocking
-
1
def value!(timeout = nil)
-
if @executor
-
super
-
else
-
result = value
-
raise @reason if @reason
-
result
-
end
-
end
-
-
# Return the value this object represents after applying the options
-
# specified by the `#set_deref_options` method.
-
#
-
# @param [Integer] timeout (nil) the maximum number of seconds to wait for
-
# the value to be computed. When `nil` the caller will block indefinitely.
-
#
-
# @return [Object] self
-
#
-
# @!macro delay_note_regarding_blocking
-
1
def wait(timeout = nil)
-
if @executor
-
execute_task_once
-
super(timeout)
-
else
-
value
-
end
-
self
-
end
-
-
# Reconfigures the block returning the value if still `#incomplete?`
-
#
-
# @yield the delayed operation to perform
-
# @return [true, false] if success
-
1
def reconfigure(&block)
-
synchronize do
-
raise ArgumentError.new('no block given') unless block_given?
-
unless @evaluation_started
-
@task = block
-
true
-
else
-
false
-
end
-
end
-
end
-
-
1
protected
-
-
1
def ns_initialize(opts, &block)
-
5
init_obligation
-
5
set_deref_options(opts)
-
5
@executor = opts[:executor]
-
-
5
@task = block
-
5
@state = :pending
-
5
@evaluation_started = false
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def execute_task_once # :nodoc:
-
# this function has been optimized for performance and
-
# should not be modified without running new benchmarks
-
execute = task = nil
-
synchronize do
-
execute = @evaluation_started = true unless @evaluation_started
-
task = @task
-
end
-
-
if execute
-
executor = Options.executor_from_options(executor: @executor)
-
executor.post do
-
begin
-
result = task.call
-
success = true
-
rescue => ex
-
reason = ex
-
end
-
synchronize do
-
set_state(success, result, reason)
-
event.set
-
end
-
end
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
-
1
Error = Class.new(StandardError)
-
-
# Raised when errors occur during configuration.
-
1
ConfigurationError = Class.new(Error)
-
-
# Raised when an asynchronous operation is cancelled before execution.
-
1
CancelledOperationError = Class.new(Error)
-
-
# Raised when a lifecycle method (such as `stop`) is called in an improper
-
# sequence or when the object is in an inappropriate state.
-
1
LifecycleError = Class.new(Error)
-
-
# Raised when an attempt is made to violate an immutability guarantee.
-
1
ImmutabilityError = Class.new(Error)
-
-
# Raised when an operation is attempted which is not legal given the
-
# receiver's current state
-
1
IllegalOperationError = Class.new(Error)
-
-
# Raised when an object's methods are called when it has not been
-
# properly initialized.
-
1
InitializationError = Class.new(Error)
-
-
# Raised when an object with a start/stop lifecycle has been started an
-
# excessive number of times. Often used in conjunction with a restart
-
# policy or strategy.
-
1
MaxRestartFrequencyError = Class.new(Error)
-
-
# Raised when an attempt is made to modify an immutable object
-
# (such as an `IVar`) after its final state has been set.
-
1
class MultipleAssignmentError < Error
-
1
attr_reader :inspection_data
-
-
1
def initialize(message = nil, inspection_data = nil)
-
@inspection_data = inspection_data
-
super message
-
end
-
-
1
def inspect
-
format '%s %s>', super[0..-2], @inspection_data.inspect
-
end
-
end
-
-
# Raised by an `Executor` when it is unable to process a given task,
-
# possibly because of a reject policy or other internal error.
-
1
RejectedExecutionError = Class.new(Error)
-
-
# Raised when any finite resource, such as a lock counter, exceeds its
-
# maximum limit/threshold.
-
1
ResourceLimitError = Class.new(Error)
-
-
# Raised when an operation times out.
-
1
TimeoutError = Class.new(Error)
-
-
# Aggregates multiple exceptions.
-
1
class MultipleErrors < Error
-
1
attr_reader :errors
-
-
1
def initialize(errors, message = "#{errors.size} errors")
-
@errors = errors
-
super [*message,
-
*errors.map { |e| [format('%s (%s)', e.message, e.class), *e.backtrace] }.flatten(1)
-
].join("\n")
-
end
-
end
-
-
end
-
1
require 'concurrent/constants'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/maybe'
-
1
require 'concurrent/atomic/atomic_reference'
-
1
require 'concurrent/atomic/count_down_latch'
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/utility/monotonic_time'
-
-
1
module Concurrent
-
-
# @!macro exchanger
-
#
-
# A synchronization point at which threads can pair and swap elements within
-
# pairs. Each thread presents some object on entry to the exchange method,
-
# matches with a partner thread, and receives its partner's object on return.
-
#
-
# @!macro thread_safe_variable_comparison
-
#
-
# This implementation is very simple, using only a single slot for each
-
# exchanger (unlike more advanced implementations which use an "arena").
-
# This approach will work perfectly fine when there are only a few threads
-
# accessing a single `Exchanger`. Beyond a handful of threads the performance
-
# will degrade rapidly due to contention on the single slot, but the algorithm
-
# will remain correct.
-
#
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Exchanger.html java.util.concurrent.Exchanger
-
# @example
-
#
-
# exchanger = Concurrent::Exchanger.new
-
#
-
# threads = [
-
# Thread.new { puts "first: " << exchanger.exchange('foo', 1) }, #=> "first: bar"
-
# Thread.new { puts "second: " << exchanger.exchange('bar', 1) } #=> "second: foo"
-
# ]
-
# threads.each {|t| t.join(2) }
-
-
# @!visibility private
-
1
class AbstractExchanger < Synchronization::Object
-
-
# @!visibility private
-
1
CANCEL = ::Object.new
-
1
private_constant :CANCEL
-
-
1
def initialize
-
super
-
end
-
-
# @!macro exchanger_method_do_exchange
-
#
-
# Waits for another thread to arrive at this exchange point (unless the
-
# current thread is interrupted), and then transfers the given object to
-
# it, receiving its object in return. The timeout value indicates the
-
# approximate number of seconds the method should block while waiting
-
# for the exchange. When the timeout value is `nil` the method will
-
# block indefinitely.
-
#
-
# @param [Object] value the value to exchange with another thread
-
# @param [Numeric, nil] timeout in seconds, `nil` blocks indefinitely
-
#
-
# @!macro exchanger_method_exchange
-
#
-
# In some edge cases when a `timeout` is given a return value of `nil` may be
-
# ambiguous. Specifically, if `nil` is a valid value in the exchange it will
-
# be impossible to tell whether `nil` is the actual return value or if it
-
# signifies timeout. When `nil` is a valid value in the exchange consider
-
# using {#exchange!} or {#try_exchange} instead.
-
#
-
# @return [Object] the value exchanged by the other thread or `nil` on timeout
-
1
def exchange(value, timeout = nil)
-
(value = do_exchange(value, timeout)) == CANCEL ? nil : value
-
end
-
-
# @!macro exchanger_method_do_exchange
-
# @!macro exchanger_method_exchange_bang
-
#
-
# On timeout a {Concurrent::TimeoutError} exception will be raised.
-
#
-
# @return [Object] the value exchanged by the other thread
-
# @raise [Concurrent::TimeoutError] on timeout
-
1
def exchange!(value, timeout = nil)
-
if (value = do_exchange(value, timeout)) == CANCEL
-
raise Concurrent::TimeoutError
-
else
-
value
-
end
-
end
-
-
# @!macro exchanger_method_do_exchange
-
# @!macro exchanger_method_try_exchange
-
#
-
# The return value will be a {Concurrent::Maybe} set to `Just` on success or
-
# `Nothing` on timeout.
-
#
-
# @return [Concurrent::Maybe] on success a `Just` maybe will be returned with
-
# the item exchanged by the other thread as `#value`; on timeout a
-
# `Nothing` maybe will be returned with {Concurrent::TimeoutError} as `#reason`
-
#
-
# @example
-
#
-
# exchanger = Concurrent::Exchanger.new
-
#
-
# result = exchanger.exchange(:foo, 0.5)
-
#
-
# if result.just?
-
# puts result.value #=> :bar
-
# else
-
# puts 'timeout'
-
# end
-
1
def try_exchange(value, timeout = nil)
-
if (value = do_exchange(value, timeout)) == CANCEL
-
Concurrent::Maybe.nothing(Concurrent::TimeoutError)
-
else
-
Concurrent::Maybe.just(value)
-
end
-
end
-
-
1
private
-
-
# @!macro exchanger_method_do_exchange
-
#
-
# @return [Object, CANCEL] the value exchanged by the other thread; {CANCEL} on timeout
-
1
def do_exchange(value, timeout)
-
raise NotImplementedError
-
end
-
end
-
-
# @!macro internal_implementation_note
-
# @!visibility private
-
1
class RubyExchanger < AbstractExchanger
-
# A simplified version of java.util.concurrent.Exchanger written by
-
# Doug Lea, Bill Scherer, and Michael Scott with assistance from members
-
# of JCP JSR-166 Expert Group and released to the public domain. It does
-
# not include the arena or the multi-processor spin loops.
-
# http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/util/concurrent/Exchanger.java
-
-
1
safe_initialization!
-
-
1
class Node < Concurrent::Synchronization::Object
-
1
attr_atomic :value
-
1
safe_initialization!
-
-
1
def initialize(item)
-
super()
-
@Item = item
-
@Latch = Concurrent::CountDownLatch.new
-
self.value = nil
-
end
-
-
1
def latch
-
@Latch
-
end
-
-
1
def item
-
@Item
-
end
-
end
-
1
private_constant :Node
-
-
1
def initialize
-
super
-
end
-
-
1
private
-
-
1
attr_atomic(:slot)
-
-
# @!macro exchanger_method_do_exchange
-
#
-
# @return [Object, CANCEL] the value exchanged by the other thread; {CANCEL} on timeout
-
1
def do_exchange(value, timeout)
-
-
# ALGORITHM
-
#
-
# From the original Java version:
-
#
-
# > The basic idea is to maintain a "slot", which is a reference to
-
# > a Node containing both an Item to offer and a "hole" waiting to
-
# > get filled in. If an incoming "occupying" thread sees that the
-
# > slot is null, it CAS'es (compareAndSets) a Node there and waits
-
# > for another to invoke exchange. That second "fulfilling" thread
-
# > sees that the slot is non-null, and so CASes it back to null,
-
# > also exchanging items by CASing the hole, plus waking up the
-
# > occupying thread if it is blocked. In each case CAS'es may
-
# > fail because a slot at first appears non-null but is null upon
-
# > CAS, or vice-versa. So threads may need to retry these
-
# > actions.
-
#
-
# This version:
-
#
-
# An exchange occurs between an "occupier" thread and a "fulfiller" thread.
-
# The "slot" is used to setup this interaction. The first thread in the
-
# exchange puts itself into the slot (occupies) and waits for a fulfiller.
-
# The second thread removes the occupier from the slot and attempts to
-
# perform the exchange. Removing the occupier also frees the slot for
-
# another occupier/fulfiller pair.
-
#
-
# Because the occupier and the fulfiller are operating independently and
-
# because there may be contention with other threads, any failed operation
-
# indicates contention. Both the occupier and the fulfiller operate within
-
# spin loops. Any failed actions along the happy path will cause the thread
-
# to repeat the loop and try again.
-
#
-
# When a timeout value is given the thread must be cognizant of time spent
-
# in the spin loop. The remaining time is checked every loop. When the time
-
# runs out the thread will exit.
-
#
-
# A "node" is the data structure used to perform the exchange. Only the
-
# occupier's node is necessary. It's the node used for the exchange.
-
# Each node has an "item," a "hole" (self), and a "latch." The item is the
-
# node's initial value. It never changes. It's what the fulfiller returns on
-
# success. The occupier's hole is where the fulfiller put its item. It's the
-
# item that the occupier returns on success. The latch is used for synchronization.
-
# Because a thread may act as either an occupier or fulfiller (or possibly
-
# both in periods of high contention) every thread creates a node when
-
# the exchange method is first called.
-
#
-
# The following steps occur within the spin loop. If any actions fail
-
# the thread will loop and try again, so long as there is time remaining.
-
# If time runs out the thread will return CANCEL.
-
#
-
# Check the slot for an occupier:
-
#
-
# * If the slot is empty try to occupy
-
# * If the slot is full try to fulfill
-
#
-
# Attempt to occupy:
-
#
-
# * Attempt to CAS myself into the slot
-
# * Go to sleep and wait to be woken by a fulfiller
-
# * If the sleep is successful then the fulfiller completed its happy path
-
# - Return the value from my hole (the value given by the fulfiller)
-
# * When the sleep fails (time ran out) attempt to cancel the operation
-
# - Attempt to CAS myself out of the hole
-
# - If successful there is no contention
-
# - Return CANCEL
-
# - On failure, I am competing with a fulfiller
-
# - Attempt to CAS my hole to CANCEL
-
# - On success
-
# - Let the fulfiller deal with my cancel
-
# - Return CANCEL
-
# - On failure the fulfiller has completed its happy path
-
# - Return th value from my hole (the fulfiller's value)
-
#
-
# Attempt to fulfill:
-
#
-
# * Attempt to CAS the occupier out of the slot
-
# - On failure loop again
-
# * Attempt to CAS my item into the occupier's hole
-
# - On failure the occupier is trying to cancel
-
# - Loop again
-
# - On success we are on the happy path
-
# - Wake the sleeping occupier
-
# - Return the occupier's item
-
-
value = NULL if value.nil? # The sentinel allows nil to be a valid value
-
me = Node.new(value) # create my node in case I need to occupy
-
end_at = Concurrent.monotonic_time + timeout.to_f # The time to give up
-
-
result = loop do
-
other = slot
-
if other && compare_and_set_slot(other, nil)
-
# try to fulfill
-
if other.compare_and_set_value(nil, value)
-
# happy path
-
other.latch.count_down
-
break other.item
-
end
-
elsif other.nil? && compare_and_set_slot(nil, me)
-
# try to occupy
-
timeout = end_at - Concurrent.monotonic_time if timeout
-
if me.latch.wait(timeout)
-
# happy path
-
break me.value
-
else
-
# attempt to remove myself from the slot
-
if compare_and_set_slot(me, nil)
-
break CANCEL
-
elsif !me.compare_and_set_value(nil, CANCEL)
-
# I've failed to block the fulfiller
-
break me.value
-
end
-
end
-
end
-
break CANCEL if timeout && Concurrent.monotonic_time >= end_at
-
end
-
-
result == NULL ? nil : result
-
end
-
end
-
-
1
if Concurrent.on_jruby?
-
-
# @!macro internal_implementation_note
-
# @!visibility private
-
class JavaExchanger < AbstractExchanger
-
-
def initialize
-
@exchanger = java.util.concurrent.Exchanger.new
-
end
-
-
private
-
-
# @!macro exchanger_method_do_exchange
-
#
-
# @return [Object, CANCEL] the value exchanged by the other thread; {CANCEL} on timeout
-
def do_exchange(value, timeout)
-
result = nil
-
if timeout.nil?
-
Synchronization::JRuby.sleep_interruptibly do
-
result = @exchanger.exchange(value)
-
end
-
else
-
Synchronization::JRuby.sleep_interruptibly do
-
result = @exchanger.exchange(value, 1000 * timeout, java.util.concurrent.TimeUnit::MILLISECONDS)
-
end
-
end
-
result
-
rescue java.util.concurrent.TimeoutException
-
CANCEL
-
end
-
end
-
end
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
ExchangerImplementation = case
-
1
when Concurrent.on_jruby?
-
JavaExchanger
-
else
-
1
RubyExchanger
-
end
-
1
private_constant :ExchangerImplementation
-
-
# @!macro exchanger
-
1
class Exchanger < ExchangerImplementation
-
-
# @!method initialize
-
# Creates exchanger instance
-
-
# @!method exchange(value, timeout = nil)
-
# @!macro exchanger_method_do_exchange
-
# @!macro exchanger_method_exchange
-
-
# @!method exchange!(value, timeout = nil)
-
# @!macro exchanger_method_do_exchange
-
# @!macro exchanger_method_exchange_bang
-
-
# @!method try_exchange(value, timeout = nil)
-
# @!macro exchanger_method_do_exchange
-
# @!macro exchanger_method_try_exchange
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/executor/thread_pool_executor'
-
-
1
module Concurrent
-
-
# A thread pool that dynamically grows and shrinks to fit the current workload.
-
# New threads are created as needed, existing threads are reused, and threads
-
# that remain idle for too long are killed and removed from the pool. These
-
# pools are particularly suited to applications that perform a high volume of
-
# short-lived tasks.
-
#
-
# On creation a `CachedThreadPool` has zero running threads. New threads are
-
# created on the pool as new operations are `#post`. The size of the pool
-
# will grow until `#max_length` threads are in the pool or until the number
-
# of threads exceeds the number of running and pending operations. When a new
-
# operation is post to the pool the first available idle thread will be tasked
-
# with the new operation.
-
#
-
# Should a thread crash for any reason the thread will immediately be removed
-
# from the pool. Similarly, threads which remain idle for an extended period
-
# of time will be killed and reclaimed. Thus these thread pools are very
-
# efficient at reclaiming unused resources.
-
#
-
# The API and behavior of this class are based on Java's `CachedThreadPool`
-
#
-
# @!macro thread_pool_options
-
1
class CachedThreadPool < ThreadPoolExecutor
-
-
# @!macro cached_thread_pool_method_initialize
-
#
-
# Create a new thread pool.
-
#
-
# @param [Hash] opts the options defining pool behavior.
-
# @option opts [Symbol] :fallback_policy (`:abort`) the fallback policy
-
#
-
# @raise [ArgumentError] if `fallback_policy` is not a known policy
-
#
-
# @see http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html#newCachedThreadPool--
-
1
def initialize(opts = {})
-
defaults = { idletime: DEFAULT_THREAD_IDLETIMEOUT }
-
overrides = { min_threads: 0,
-
max_threads: DEFAULT_MAX_POOL_SIZE,
-
max_queue: DEFAULT_MAX_QUEUE_SIZE }
-
super(defaults.merge(opts).merge(overrides))
-
end
-
-
1
private
-
-
# @!macro cached_thread_pool_method_initialize
-
# @!visibility private
-
1
def ns_initialize(opts)
-
super(opts)
-
if Concurrent.on_jruby?
-
@max_queue = 0
-
@executor = java.util.concurrent.Executors.newCachedThreadPool(
-
DaemonThreadFactory.new(ns_auto_terminate?))
-
@executor.setRejectedExecutionHandler(FALLBACK_POLICY_CLASSES[@fallback_policy].new)
-
@executor.setKeepAliveTime(opts.fetch(:idletime, DEFAULT_THREAD_IDLETIMEOUT), java.util.concurrent.TimeUnit::SECONDS)
-
end
-
end
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/executor/thread_pool_executor'
-
-
1
module Concurrent
-
-
# @!macro thread_pool_executor_constant_default_max_pool_size
-
# Default maximum number of threads that will be created in the pool.
-
-
# @!macro thread_pool_executor_constant_default_min_pool_size
-
# Default minimum number of threads that will be retained in the pool.
-
-
# @!macro thread_pool_executor_constant_default_max_queue_size
-
# Default maximum number of tasks that may be added to the task queue.
-
-
# @!macro thread_pool_executor_constant_default_thread_timeout
-
# Default maximum number of seconds a thread in the pool may remain idle
-
# before being reclaimed.
-
-
# @!macro thread_pool_executor_attr_reader_max_length
-
# The maximum number of threads that may be created in the pool.
-
# @return [Integer] The maximum number of threads that may be created in the pool.
-
-
# @!macro thread_pool_executor_attr_reader_min_length
-
# The minimum number of threads that may be retained in the pool.
-
# @return [Integer] The minimum number of threads that may be retained in the pool.
-
-
# @!macro thread_pool_executor_attr_reader_largest_length
-
# The largest number of threads that have been created in the pool since construction.
-
# @return [Integer] The largest number of threads that have been created in the pool since construction.
-
-
# @!macro thread_pool_executor_attr_reader_scheduled_task_count
-
# The number of tasks that have been scheduled for execution on the pool since construction.
-
# @return [Integer] The number of tasks that have been scheduled for execution on the pool since construction.
-
-
# @!macro thread_pool_executor_attr_reader_completed_task_count
-
# The number of tasks that have been completed by the pool since construction.
-
# @return [Integer] The number of tasks that have been completed by the pool since construction.
-
-
# @!macro thread_pool_executor_attr_reader_idletime
-
# The number of seconds that a thread may be idle before being reclaimed.
-
# @return [Integer] The number of seconds that a thread may be idle before being reclaimed.
-
-
# @!macro thread_pool_executor_attr_reader_max_queue
-
# The maximum number of tasks that may be waiting in the work queue at any one time.
-
# When the queue size reaches `max_queue` subsequent tasks will be rejected in
-
# accordance with the configured `fallback_policy`.
-
#
-
# @return [Integer] The maximum number of tasks that may be waiting in the work queue at any one time.
-
# When the queue size reaches `max_queue` subsequent tasks will be rejected in
-
# accordance with the configured `fallback_policy`.
-
-
# @!macro thread_pool_executor_attr_reader_length
-
# The number of threads currently in the pool.
-
# @return [Integer] The number of threads currently in the pool.
-
-
# @!macro thread_pool_executor_attr_reader_queue_length
-
# The number of tasks in the queue awaiting execution.
-
# @return [Integer] The number of tasks in the queue awaiting execution.
-
-
# @!macro thread_pool_executor_attr_reader_remaining_capacity
-
# Number of tasks that may be enqueued before reaching `max_queue` and rejecting
-
# new tasks. A value of -1 indicates that the queue may grow without bound.
-
#
-
# @return [Integer] Number of tasks that may be enqueued before reaching `max_queue` and rejecting
-
# new tasks. A value of -1 indicates that the queue may grow without bound.
-
-
-
-
-
-
# @!macro thread_pool_executor_public_api
-
#
-
# @!macro abstract_executor_service_public_api
-
#
-
# @!attribute [r] max_length
-
# @!macro thread_pool_executor_attr_reader_max_length
-
#
-
# @!attribute [r] min_length
-
# @!macro thread_pool_executor_attr_reader_min_length
-
#
-
# @!attribute [r] largest_length
-
# @!macro thread_pool_executor_attr_reader_largest_length
-
#
-
# @!attribute [r] scheduled_task_count
-
# @!macro thread_pool_executor_attr_reader_scheduled_task_count
-
#
-
# @!attribute [r] completed_task_count
-
# @!macro thread_pool_executor_attr_reader_completed_task_count
-
#
-
# @!attribute [r] idletime
-
# @!macro thread_pool_executor_attr_reader_idletime
-
#
-
# @!attribute [r] max_queue
-
# @!macro thread_pool_executor_attr_reader_max_queue
-
#
-
# @!attribute [r] length
-
# @!macro thread_pool_executor_attr_reader_length
-
#
-
# @!attribute [r] queue_length
-
# @!macro thread_pool_executor_attr_reader_queue_length
-
#
-
# @!attribute [r] remaining_capacity
-
# @!macro thread_pool_executor_attr_reader_remaining_capacity
-
#
-
# @!method can_overflow?
-
# @!macro executor_service_method_can_overflow_question
-
-
-
-
-
# @!macro thread_pool_options
-
#
-
# **Thread Pool Options**
-
#
-
# Thread pools support several configuration options:
-
#
-
# * `idletime`: The number of seconds that a thread may be idle before being reclaimed.
-
# * `name`: The name of the executor (optional). Printed in the executor's `#to_s` output and
-
# a `<name>-worker-<id>` name is given to its threads if supported by used Ruby
-
# implementation. `<id>` is uniq for each thread.
-
# * `max_queue`: The maximum number of tasks that may be waiting in the work queue at
-
# any one time. When the queue size reaches `max_queue` and no new threads can be created,
-
# subsequent tasks will be rejected in accordance with the configured `fallback_policy`.
-
# * `auto_terminate`: When true (default), the threads started will be marked as daemon.
-
# * `fallback_policy`: The policy defining how rejected tasks are handled.
-
#
-
# Three fallback policies are supported:
-
#
-
# * `:abort`: Raise a `RejectedExecutionError` exception and discard the task.
-
# * `:discard`: Discard the task and return false.
-
# * `:caller_runs`: Execute the task on the calling thread.
-
#
-
# **Shutting Down Thread Pools**
-
#
-
# Killing a thread pool while tasks are still being processed, either by calling
-
# the `#kill` method or at application exit, will have unpredictable results. There
-
# is no way for the thread pool to know what resources are being used by the
-
# in-progress tasks. When those tasks are killed the impact on those resources
-
# cannot be predicted. The *best* practice is to explicitly shutdown all thread
-
# pools using the provided methods:
-
#
-
# * Call `#shutdown` to initiate an orderly termination of all in-progress tasks
-
# * Call `#wait_for_termination` with an appropriate timeout interval an allow
-
# the orderly shutdown to complete
-
# * Call `#kill` *only when* the thread pool fails to shutdown in the allotted time
-
#
-
# On some runtime platforms (most notably the JVM) the application will not
-
# exit until all thread pools have been shutdown. To prevent applications from
-
# "hanging" on exit, all threads can be marked as daemon according to the
-
# `:auto_terminate` option.
-
#
-
# ```ruby
-
# pool1 = Concurrent::FixedThreadPool.new(5) # threads will be marked as daemon
-
# pool2 = Concurrent::FixedThreadPool.new(5, auto_terminate: false) # mark threads as non-daemon
-
# ```
-
#
-
# @note Failure to properly shutdown a thread pool can lead to unpredictable results.
-
# Please read *Shutting Down Thread Pools* for more information.
-
#
-
# @see http://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html Java Tutorials: Thread Pools
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Executors.html Java Executors class
-
# @see http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html Java ExecutorService interface
-
# @see https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#setDaemon-boolean-
-
-
-
-
-
-
# @!macro fixed_thread_pool
-
#
-
# A thread pool that reuses a fixed number of threads operating off an unbounded queue.
-
# At any point, at most `num_threads` will be active processing tasks. When all threads are busy new
-
# tasks `#post` to the thread pool are enqueued until a thread becomes available.
-
# Should a thread crash for any reason the thread will immediately be removed
-
# from the pool and replaced.
-
#
-
# The API and behavior of this class are based on Java's `FixedThreadPool`
-
#
-
# @!macro thread_pool_options
-
1
class FixedThreadPool < ThreadPoolExecutor
-
-
# @!macro fixed_thread_pool_method_initialize
-
#
-
# Create a new thread pool.
-
#
-
# @param [Integer] num_threads the number of threads to allocate
-
# @param [Hash] opts the options defining pool behavior.
-
# @option opts [Symbol] :fallback_policy (`:abort`) the fallback policy
-
#
-
# @raise [ArgumentError] if `num_threads` is less than or equal to zero
-
# @raise [ArgumentError] if `fallback_policy` is not a known policy
-
#
-
# @see http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html#newFixedThreadPool-int-
-
1
def initialize(num_threads, opts = {})
-
raise ArgumentError.new('number of threads must be greater than zero') if num_threads.to_i < 1
-
defaults = { max_queue: DEFAULT_MAX_QUEUE_SIZE,
-
idletime: DEFAULT_THREAD_IDLETIMEOUT }
-
overrides = { min_threads: num_threads,
-
max_threads: num_threads }
-
super(defaults.merge(opts).merge(overrides))
-
end
-
end
-
end
-
1
require 'concurrent/atomic/event'
-
1
require 'concurrent/executor/abstract_executor_service'
-
1
require 'concurrent/executor/serial_executor_service'
-
-
1
module Concurrent
-
-
# An executor service which runs all operations on the current thread,
-
# blocking as necessary. Operations are performed in the order they are
-
# received and no two operations can be performed simultaneously.
-
#
-
# This executor service exists mainly for testing an debugging. When used
-
# it immediately runs every `#post` operation on the current thread, blocking
-
# that thread until the operation is complete. This can be very beneficial
-
# during testing because it makes all operations deterministic.
-
#
-
# @note Intended for use primarily in testing and debugging.
-
1
class ImmediateExecutor < AbstractExecutorService
-
1
include SerialExecutorService
-
-
# Creates a new executor
-
1
def initialize
-
1
@stopped = Concurrent::Event.new
-
end
-
-
# @!macro executor_service_method_post
-
1
def post(*args, &task)
-
raise ArgumentError.new('no block given') unless block_given?
-
return false unless running?
-
task.call(*args)
-
true
-
end
-
-
# @!macro executor_service_method_left_shift
-
1
def <<(task)
-
post(&task)
-
self
-
end
-
-
# @!macro executor_service_method_running_question
-
1
def running?
-
! shutdown?
-
end
-
-
# @!macro executor_service_method_shuttingdown_question
-
1
def shuttingdown?
-
false
-
end
-
-
# @!macro executor_service_method_shutdown_question
-
1
def shutdown?
-
@stopped.set?
-
end
-
-
# @!macro executor_service_method_shutdown
-
1
def shutdown
-
@stopped.set
-
true
-
end
-
1
alias_method :kill, :shutdown
-
-
# @!macro executor_service_method_wait_for_termination
-
1
def wait_for_termination(timeout = nil)
-
@stopped.wait(timeout)
-
end
-
end
-
end
-
1
require 'concurrent/executor/immediate_executor'
-
1
require 'concurrent/executor/simple_executor_service'
-
-
1
module Concurrent
-
# An executor service which runs all operations on a new thread, blocking
-
# until it completes. Operations are performed in the order they are received
-
# and no two operations can be performed simultaneously.
-
#
-
# This executor service exists mainly for testing an debugging. When used it
-
# immediately runs every `#post` operation on a new thread, blocking the
-
# current thread until the operation is complete. This is similar to how the
-
# ImmediateExecutor works, but the operation has the full stack of the new
-
# thread at its disposal. This can be helpful when the operations will spawn
-
# more operations on the same executor and so on - such a situation might
-
# overflow the single stack in case of an ImmediateExecutor, which is
-
# inconsistent with how it would behave for a threaded executor.
-
#
-
# @note Intended for use primarily in testing and debugging.
-
1
class IndirectImmediateExecutor < ImmediateExecutor
-
# Creates a new executor
-
1
def initialize
-
super
-
@internal_executor = SimpleExecutorService.new
-
end
-
-
# @!macro executor_service_method_post
-
1
def post(*args, &task)
-
raise ArgumentError.new("no block given") unless block_given?
-
return false unless running?
-
-
event = Concurrent::Event.new
-
@internal_executor.post do
-
begin
-
task.call(*args)
-
ensure
-
event.set
-
end
-
end
-
event.wait
-
-
true
-
end
-
end
-
end
-
1
if Concurrent.on_jruby?
-
-
require 'concurrent/executor/java_executor_service'
-
require 'concurrent/executor/serial_executor_service'
-
-
module Concurrent
-
-
# @!macro single_thread_executor
-
# @!macro abstract_executor_service_public_api
-
# @!visibility private
-
class JavaSingleThreadExecutor < JavaExecutorService
-
include SerialExecutorService
-
-
# @!macro single_thread_executor_method_initialize
-
def initialize(opts = {})
-
super(opts)
-
end
-
-
private
-
-
def ns_initialize(opts)
-
@executor = java.util.concurrent.Executors.newSingleThreadExecutor(
-
DaemonThreadFactory.new(ns_auto_terminate?)
-
)
-
@fallback_policy = opts.fetch(:fallback_policy, :discard)
-
raise ArgumentError.new("#{@fallback_policy} is not a valid fallback policy") unless FALLBACK_POLICY_CLASSES.keys.include?(@fallback_policy)
-
end
-
end
-
end
-
end
-
1
if Concurrent.on_jruby?
-
-
require 'concurrent/executor/java_executor_service'
-
-
module Concurrent
-
-
# @!macro thread_pool_executor
-
# @!macro thread_pool_options
-
# @!visibility private
-
class JavaThreadPoolExecutor < JavaExecutorService
-
-
# @!macro thread_pool_executor_constant_default_max_pool_size
-
DEFAULT_MAX_POOL_SIZE = java.lang.Integer::MAX_VALUE # 2147483647
-
-
# @!macro thread_pool_executor_constant_default_min_pool_size
-
DEFAULT_MIN_POOL_SIZE = 0
-
-
# @!macro thread_pool_executor_constant_default_max_queue_size
-
DEFAULT_MAX_QUEUE_SIZE = 0
-
-
# @!macro thread_pool_executor_constant_default_thread_timeout
-
DEFAULT_THREAD_IDLETIMEOUT = 60
-
-
# @!macro thread_pool_executor_attr_reader_max_length
-
attr_reader :max_length
-
-
# @!macro thread_pool_executor_attr_reader_max_queue
-
attr_reader :max_queue
-
-
# @!macro thread_pool_executor_method_initialize
-
def initialize(opts = {})
-
super(opts)
-
end
-
-
# @!macro executor_service_method_can_overflow_question
-
def can_overflow?
-
@max_queue != 0
-
end
-
-
# @!macro thread_pool_executor_attr_reader_min_length
-
def min_length
-
@executor.getCorePoolSize
-
end
-
-
# @!macro thread_pool_executor_attr_reader_max_length
-
def max_length
-
@executor.getMaximumPoolSize
-
end
-
-
# @!macro thread_pool_executor_attr_reader_length
-
def length
-
@executor.getPoolSize
-
end
-
-
# @!macro thread_pool_executor_attr_reader_largest_length
-
def largest_length
-
@executor.getLargestPoolSize
-
end
-
-
# @!macro thread_pool_executor_attr_reader_scheduled_task_count
-
def scheduled_task_count
-
@executor.getTaskCount
-
end
-
-
# @!macro thread_pool_executor_attr_reader_completed_task_count
-
def completed_task_count
-
@executor.getCompletedTaskCount
-
end
-
-
# @!macro thread_pool_executor_attr_reader_idletime
-
def idletime
-
@executor.getKeepAliveTime(java.util.concurrent.TimeUnit::SECONDS)
-
end
-
-
# @!macro thread_pool_executor_attr_reader_queue_length
-
def queue_length
-
@executor.getQueue.size
-
end
-
-
# @!macro thread_pool_executor_attr_reader_remaining_capacity
-
def remaining_capacity
-
@max_queue == 0 ? -1 : @executor.getQueue.remainingCapacity
-
end
-
-
# @!macro executor_service_method_running_question
-
def running?
-
super && !@executor.isTerminating
-
end
-
-
private
-
-
def ns_initialize(opts)
-
min_length = opts.fetch(:min_threads, DEFAULT_MIN_POOL_SIZE).to_i
-
max_length = opts.fetch(:max_threads, DEFAULT_MAX_POOL_SIZE).to_i
-
idletime = opts.fetch(:idletime, DEFAULT_THREAD_IDLETIMEOUT).to_i
-
@max_queue = opts.fetch(:max_queue, DEFAULT_MAX_QUEUE_SIZE).to_i
-
@fallback_policy = opts.fetch(:fallback_policy, :abort)
-
-
raise ArgumentError.new("`max_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}") if max_length < DEFAULT_MIN_POOL_SIZE
-
raise ArgumentError.new("`max_threads` cannot be greater than #{DEFAULT_MAX_POOL_SIZE}") if max_length > DEFAULT_MAX_POOL_SIZE
-
raise ArgumentError.new("`min_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}") if min_length < DEFAULT_MIN_POOL_SIZE
-
raise ArgumentError.new("`min_threads` cannot be more than `max_threads`") if min_length > max_length
-
raise ArgumentError.new("#{fallback_policy} is not a valid fallback policy") unless FALLBACK_POLICY_CLASSES.include?(@fallback_policy)
-
-
if @max_queue == 0
-
queue = java.util.concurrent.LinkedBlockingQueue.new
-
else
-
queue = java.util.concurrent.LinkedBlockingQueue.new(@max_queue)
-
end
-
-
@executor = java.util.concurrent.ThreadPoolExecutor.new(
-
min_length,
-
max_length,
-
idletime,
-
java.util.concurrent.TimeUnit::SECONDS,
-
queue,
-
DaemonThreadFactory.new(ns_auto_terminate?),
-
FALLBACK_POLICY_CLASSES[@fallback_policy].new)
-
-
end
-
end
-
-
end
-
end
-
1
require 'concurrent/executor/abstract_executor_service'
-
1
require 'concurrent/atomic/event'
-
-
1
module Concurrent
-
-
# @!macro abstract_executor_service_public_api
-
# @!visibility private
-
1
class RubyExecutorService < AbstractExecutorService
-
1
safe_initialization!
-
-
1
def initialize(*args, &block)
-
super
-
@StopEvent = Event.new
-
@StoppedEvent = Event.new
-
end
-
-
1
def post(*args, &task)
-
raise ArgumentError.new('no block given') unless block_given?
-
synchronize do
-
# If the executor is shut down, reject this task
-
return handle_fallback(*args, &task) unless running?
-
ns_execute(*args, &task)
-
true
-
end
-
end
-
-
1
def shutdown
-
synchronize do
-
break unless running?
-
stop_event.set
-
ns_shutdown_execution
-
end
-
true
-
end
-
-
1
def kill
-
synchronize do
-
break if shutdown?
-
stop_event.set
-
ns_kill_execution
-
stopped_event.set
-
end
-
true
-
end
-
-
1
def wait_for_termination(timeout = nil)
-
stopped_event.wait(timeout)
-
end
-
-
1
private
-
-
1
def stop_event
-
@StopEvent
-
end
-
-
1
def stopped_event
-
@StoppedEvent
-
end
-
-
1
def ns_shutdown_execution
-
stopped_event.set
-
end
-
-
1
def ns_running?
-
!stop_event.set?
-
end
-
-
1
def ns_shuttingdown?
-
!(ns_running? || ns_shutdown?)
-
end
-
-
1
def ns_shutdown?
-
stopped_event.set?
-
end
-
end
-
end
-
1
require 'concurrent/executor/ruby_thread_pool_executor'
-
-
1
module Concurrent
-
-
# @!macro single_thread_executor
-
# @!macro abstract_executor_service_public_api
-
# @!visibility private
-
1
class RubySingleThreadExecutor < RubyThreadPoolExecutor
-
-
# @!macro single_thread_executor_method_initialize
-
1
def initialize(opts = {})
-
super(
-
min_threads: 1,
-
max_threads: 1,
-
max_queue: 0,
-
idletime: DEFAULT_THREAD_IDLETIMEOUT,
-
fallback_policy: opts.fetch(:fallback_policy, :discard),
-
)
-
end
-
end
-
end
-
1
require 'thread'
-
1
require 'concurrent/atomic/event'
-
1
require 'concurrent/concern/logging'
-
1
require 'concurrent/executor/ruby_executor_service'
-
1
require 'concurrent/utility/monotonic_time'
-
-
1
module Concurrent
-
-
# @!macro thread_pool_executor
-
# @!macro thread_pool_options
-
# @!visibility private
-
1
class RubyThreadPoolExecutor < RubyExecutorService
-
-
# @!macro thread_pool_executor_constant_default_max_pool_size
-
1
DEFAULT_MAX_POOL_SIZE = 2_147_483_647 # java.lang.Integer::MAX_VALUE
-
-
# @!macro thread_pool_executor_constant_default_min_pool_size
-
1
DEFAULT_MIN_POOL_SIZE = 0
-
-
# @!macro thread_pool_executor_constant_default_max_queue_size
-
1
DEFAULT_MAX_QUEUE_SIZE = 0
-
-
# @!macro thread_pool_executor_constant_default_thread_timeout
-
1
DEFAULT_THREAD_IDLETIMEOUT = 60
-
-
# @!macro thread_pool_executor_attr_reader_max_length
-
1
attr_reader :max_length
-
-
# @!macro thread_pool_executor_attr_reader_min_length
-
1
attr_reader :min_length
-
-
# @!macro thread_pool_executor_attr_reader_idletime
-
1
attr_reader :idletime
-
-
# @!macro thread_pool_executor_attr_reader_max_queue
-
1
attr_reader :max_queue
-
-
# @!macro thread_pool_executor_method_initialize
-
1
def initialize(opts = {})
-
super(opts)
-
end
-
-
# @!macro thread_pool_executor_attr_reader_largest_length
-
1
def largest_length
-
synchronize { @largest_length }
-
end
-
-
# @!macro thread_pool_executor_attr_reader_scheduled_task_count
-
1
def scheduled_task_count
-
synchronize { @scheduled_task_count }
-
end
-
-
# @!macro thread_pool_executor_attr_reader_completed_task_count
-
1
def completed_task_count
-
synchronize { @completed_task_count }
-
end
-
-
# @!macro executor_service_method_can_overflow_question
-
1
def can_overflow?
-
synchronize { ns_limited_queue? }
-
end
-
-
# @!macro thread_pool_executor_attr_reader_length
-
1
def length
-
synchronize { @pool.length }
-
end
-
-
# @!macro thread_pool_executor_attr_reader_queue_length
-
1
def queue_length
-
synchronize { @queue.length }
-
end
-
-
# @!macro thread_pool_executor_attr_reader_remaining_capacity
-
1
def remaining_capacity
-
synchronize do
-
if ns_limited_queue?
-
@max_queue - @queue.length
-
else
-
-1
-
end
-
end
-
end
-
-
# @!visibility private
-
1
def remove_busy_worker(worker)
-
synchronize { ns_remove_busy_worker worker }
-
end
-
-
# @!visibility private
-
1
def ready_worker(worker)
-
synchronize { ns_ready_worker worker }
-
end
-
-
# @!visibility private
-
1
def worker_not_old_enough(worker)
-
synchronize { ns_worker_not_old_enough worker }
-
end
-
-
# @!visibility private
-
1
def worker_died(worker)
-
synchronize { ns_worker_died worker }
-
end
-
-
# @!visibility private
-
1
def worker_task_completed
-
synchronize { @completed_task_count += 1 }
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def ns_initialize(opts)
-
@min_length = opts.fetch(:min_threads, DEFAULT_MIN_POOL_SIZE).to_i
-
@max_length = opts.fetch(:max_threads, DEFAULT_MAX_POOL_SIZE).to_i
-
@idletime = opts.fetch(:idletime, DEFAULT_THREAD_IDLETIMEOUT).to_i
-
@max_queue = opts.fetch(:max_queue, DEFAULT_MAX_QUEUE_SIZE).to_i
-
@fallback_policy = opts.fetch(:fallback_policy, :abort)
-
raise ArgumentError.new("#{@fallback_policy} is not a valid fallback policy") unless FALLBACK_POLICIES.include?(@fallback_policy)
-
-
raise ArgumentError.new("`max_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}") if @max_length < DEFAULT_MIN_POOL_SIZE
-
raise ArgumentError.new("`max_threads` cannot be greater than #{DEFAULT_MAX_POOL_SIZE}") if @max_length > DEFAULT_MAX_POOL_SIZE
-
raise ArgumentError.new("`min_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}") if @min_length < DEFAULT_MIN_POOL_SIZE
-
raise ArgumentError.new("`min_threads` cannot be more than `max_threads`") if min_length > max_length
-
-
@pool = [] # all workers
-
@ready = [] # used as a stash (most idle worker is at the start)
-
@queue = [] # used as queue
-
# @ready or @queue is empty at all times
-
@scheduled_task_count = 0
-
@completed_task_count = 0
-
@largest_length = 0
-
@workers_counter = 0
-
@ruby_pid = $$ # detects if Ruby has forked
-
-
@gc_interval = opts.fetch(:gc_interval, @idletime / 2.0).to_i # undocumented
-
@next_gc_time = Concurrent.monotonic_time + @gc_interval
-
end
-
-
# @!visibility private
-
1
def ns_limited_queue?
-
@max_queue != 0
-
end
-
-
# @!visibility private
-
1
def ns_execute(*args, &task)
-
ns_reset_if_forked
-
-
if ns_assign_worker(*args, &task) || ns_enqueue(*args, &task)
-
@scheduled_task_count += 1
-
else
-
handle_fallback(*args, &task)
-
end
-
-
ns_prune_pool if @next_gc_time < Concurrent.monotonic_time
-
end
-
-
# @!visibility private
-
1
def ns_shutdown_execution
-
ns_reset_if_forked
-
-
if @pool.empty?
-
# nothing to do
-
stopped_event.set
-
end
-
-
if @queue.empty?
-
# no more tasks will be accepted, just stop all workers
-
@pool.each(&:stop)
-
end
-
end
-
-
# @!visibility private
-
1
def ns_kill_execution
-
# TODO log out unprocessed tasks in queue
-
# TODO try to shutdown first?
-
@pool.each(&:kill)
-
@pool.clear
-
@ready.clear
-
end
-
-
# tries to assign task to a worker, tries to get one from @ready or to create new one
-
# @return [true, false] if task is assigned to a worker
-
#
-
# @!visibility private
-
1
def ns_assign_worker(*args, &task)
-
# keep growing if the pool is not at the minimum yet
-
worker = (@ready.pop if @pool.size >= @min_length) || ns_add_busy_worker
-
if worker
-
worker << [task, args]
-
true
-
else
-
false
-
end
-
rescue ThreadError
-
# Raised when the operating system refuses to create the new thread
-
return false
-
end
-
-
# tries to enqueue task
-
# @return [true, false] if enqueued
-
#
-
# @!visibility private
-
1
def ns_enqueue(*args, &task)
-
if !ns_limited_queue? || @queue.size < @max_queue
-
@queue << [task, args]
-
true
-
else
-
false
-
end
-
end
-
-
# @!visibility private
-
1
def ns_worker_died(worker)
-
ns_remove_busy_worker worker
-
replacement_worker = ns_add_busy_worker
-
ns_ready_worker replacement_worker, false if replacement_worker
-
end
-
-
# creates new worker which has to receive work to do after it's added
-
# @return [nil, Worker] nil of max capacity is reached
-
#
-
# @!visibility private
-
1
def ns_add_busy_worker
-
return if @pool.size >= @max_length
-
-
@workers_counter += 1
-
@pool << (worker = Worker.new(self, @workers_counter))
-
@largest_length = @pool.length if @pool.length > @largest_length
-
worker
-
end
-
-
# handle ready worker, giving it new job or assigning back to @ready
-
#
-
# @!visibility private
-
1
def ns_ready_worker(worker, success = true)
-
task_and_args = @queue.shift
-
if task_and_args
-
worker << task_and_args
-
else
-
# stop workers when !running?, do not return them to @ready
-
if running?
-
@ready.push(worker)
-
else
-
worker.stop
-
end
-
end
-
end
-
-
# returns back worker to @ready which was not idle for enough time
-
#
-
# @!visibility private
-
1
def ns_worker_not_old_enough(worker)
-
# let's put workers coming from idle_test back to the start (as the oldest worker)
-
@ready.unshift(worker)
-
true
-
end
-
-
# removes a worker which is not in not tracked in @ready
-
#
-
# @!visibility private
-
1
def ns_remove_busy_worker(worker)
-
@pool.delete(worker)
-
stopped_event.set if @pool.empty? && !running?
-
true
-
end
-
-
# try oldest worker if it is idle for enough time, it's returned back at the start
-
#
-
# @!visibility private
-
1
def ns_prune_pool
-
return if @pool.size <= @min_length
-
-
last_used = @ready.shift
-
last_used << :idle_test if last_used
-
-
@next_gc_time = Concurrent.monotonic_time + @gc_interval
-
end
-
-
1
def ns_reset_if_forked
-
if $$ != @ruby_pid
-
@queue.clear
-
@ready.clear
-
@pool.clear
-
@scheduled_task_count = 0
-
@completed_task_count = 0
-
@largest_length = 0
-
@workers_counter = 0
-
@ruby_pid = $$
-
end
-
end
-
-
# @!visibility private
-
1
class Worker
-
1
include Concern::Logging
-
-
1
def initialize(pool, id)
-
# instance variables accessed only under pool's lock so no need to sync here again
-
@queue = Queue.new
-
@pool = pool
-
@thread = create_worker @queue, pool, pool.idletime
-
-
if @thread.respond_to?(:name=)
-
@thread.name = [pool.name, 'worker', id].compact.join('-')
-
end
-
end
-
-
1
def <<(message)
-
@queue << message
-
end
-
-
1
def stop
-
@queue << :stop
-
end
-
-
1
def kill
-
@thread.kill
-
end
-
-
1
private
-
-
1
def create_worker(queue, pool, idletime)
-
Thread.new(queue, pool, idletime) do |my_queue, my_pool, my_idletime|
-
last_message = Concurrent.monotonic_time
-
catch(:stop) do
-
loop do
-
-
case message = my_queue.pop
-
when :idle_test
-
if (Concurrent.monotonic_time - last_message) > my_idletime
-
my_pool.remove_busy_worker(self)
-
throw :stop
-
else
-
my_pool.worker_not_old_enough(self)
-
end
-
-
when :stop
-
my_pool.remove_busy_worker(self)
-
throw :stop
-
-
else
-
task, args = message
-
run_task my_pool, task, args
-
last_message = Concurrent.monotonic_time
-
-
my_pool.ready_worker(self)
-
end
-
end
-
end
-
end
-
end
-
-
1
def run_task(pool, task, args)
-
task.call(*args)
-
pool.worker_task_completed
-
rescue => ex
-
# let it fail
-
log DEBUG, ex
-
rescue Exception => ex
-
log ERROR, ex
-
pool.worker_died(self)
-
throw :stop
-
end
-
end
-
-
1
private_constant :Worker
-
end
-
end
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# A simple utility class that executes a callable and returns and array of three elements:
-
# success - indicating if the callable has been executed without errors
-
# value - filled by the callable result if it has been executed without errors, nil otherwise
-
# reason - the error risen by the callable if it has been executed with errors, nil otherwise
-
1
class SafeTaskExecutor < Synchronization::LockableObject
-
-
1
def initialize(task, opts = {})
-
@task = task
-
@exception_class = opts.fetch(:rescue_exception, false) ? Exception : StandardError
-
super() # ensures visibility
-
end
-
-
# @return [Array]
-
1
def execute(*args)
-
synchronize do
-
success = false
-
value = reason = nil
-
-
begin
-
value = @task.call(*args)
-
success = true
-
rescue @exception_class => ex
-
reason = ex
-
success = false
-
end
-
-
[success, value, reason]
-
end
-
end
-
end
-
end
-
1
require 'concurrent/executor/executor_service'
-
-
1
module Concurrent
-
-
# Indicates that the including `ExecutorService` guarantees
-
# that all operations will occur in the order they are post and that no
-
# two operations may occur simultaneously. This module provides no
-
# functionality and provides no guarantees. That is the responsibility
-
# of the including class. This module exists solely to allow the including
-
# object to be interrogated for its serialization status.
-
#
-
# @example
-
# class Foo
-
# include Concurrent::SerialExecutor
-
# end
-
#
-
# foo = Foo.new
-
#
-
# foo.is_a? Concurrent::ExecutorService #=> true
-
# foo.is_a? Concurrent::SerialExecutor #=> true
-
# foo.serialized? #=> true
-
#
-
# @!visibility private
-
1
module SerialExecutorService
-
1
include ExecutorService
-
-
# @!macro executor_service_method_serialized_question
-
#
-
# @note Always returns `true`
-
1
def serialized?
-
true
-
end
-
end
-
end
-
1
require 'concurrent/errors'
-
1
require 'concurrent/concern/logging'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# Ensures passed jobs in a serialized order never running at the same time.
-
1
class SerializedExecution < Synchronization::LockableObject
-
1
include Concern::Logging
-
-
1
def initialize()
-
super()
-
synchronize { ns_initialize }
-
end
-
-
1
Job = Struct.new(:executor, :args, :block) do
-
1
def call
-
block.call(*args)
-
end
-
end
-
-
# Submit a task to the executor for asynchronous processing.
-
#
-
# @param [Executor] executor to be used for this job
-
#
-
# @param [Array] args zero or more arguments to be passed to the task
-
#
-
# @yield the asynchronous task to perform
-
#
-
# @return [Boolean] `true` if the task is queued, `false` if the executor
-
# is not running
-
#
-
# @raise [ArgumentError] if no task is given
-
1
def post(executor, *args, &task)
-
posts [[executor, args, task]]
-
true
-
end
-
-
# As {#post} but allows to submit multiple tasks at once, it's guaranteed that they will not
-
# be interleaved by other tasks.
-
#
-
# @param [Array<Array(ExecutorService, Array<Object>, Proc)>] posts array of triplets where
-
# first is a {ExecutorService}, second is array of args for task, third is a task (Proc)
-
1
def posts(posts)
-
# if can_overflow?
-
# raise ArgumentError, 'SerializedExecution does not support thread-pools which can overflow'
-
# end
-
-
return nil if posts.empty?
-
-
jobs = posts.map { |executor, args, task| Job.new executor, args, task }
-
-
job_to_post = synchronize do
-
if @being_executed
-
@stash.push(*jobs)
-
nil
-
else
-
@being_executed = true
-
@stash.push(*jobs[1..-1])
-
jobs.first
-
end
-
end
-
-
call_job job_to_post if job_to_post
-
true
-
end
-
-
1
private
-
-
1
def ns_initialize
-
@being_executed = false
-
@stash = []
-
end
-
-
1
def call_job(job)
-
did_it_run = begin
-
job.executor.post { work(job) }
-
true
-
rescue RejectedExecutionError => ex
-
false
-
end
-
-
# TODO not the best idea to run it myself
-
unless did_it_run
-
begin
-
work job
-
rescue => ex
-
# let it fail
-
log DEBUG, ex
-
end
-
end
-
end
-
-
# ensures next job is executed if any is stashed
-
1
def work(job)
-
job.call
-
ensure
-
synchronize do
-
job = @stash.shift || (@being_executed = false)
-
end
-
-
# TODO maybe be able to tell caching pool to just enqueue this job, because the current one end at the end
-
# of this block
-
call_job job if job
-
end
-
end
-
end
-
1
require 'delegate'
-
1
require 'concurrent/executor/serial_executor_service'
-
1
require 'concurrent/executor/serialized_execution'
-
-
1
module Concurrent
-
-
# A wrapper/delegator for any `ExecutorService` that
-
# guarantees serialized execution of tasks.
-
#
-
# @see [SimpleDelegator](http://www.ruby-doc.org/stdlib-2.1.2/libdoc/delegate/rdoc/SimpleDelegator.html)
-
# @see Concurrent::SerializedExecution
-
1
class SerializedExecutionDelegator < SimpleDelegator
-
1
include SerialExecutorService
-
-
1
def initialize(executor)
-
@executor = executor
-
@serializer = SerializedExecution.new
-
super(executor)
-
end
-
-
# @!macro executor_service_method_post
-
1
def post(*args, &task)
-
raise ArgumentError.new('no block given') unless block_given?
-
return false unless running?
-
@serializer.post(@executor, *args, &task)
-
end
-
end
-
end
-
1
require 'concurrent/atomics'
-
1
require 'concurrent/executor/executor_service'
-
-
1
module Concurrent
-
-
# An executor service in which every operation spawns a new,
-
# independently operating thread.
-
#
-
# This is perhaps the most inefficient executor service in this
-
# library. It exists mainly for testing an debugging. Thread creation
-
# and management is expensive in Ruby and this executor performs no
-
# resource pooling. This can be very beneficial during testing and
-
# debugging because it decouples the using code from the underlying
-
# executor implementation. In production this executor will likely
-
# lead to suboptimal performance.
-
#
-
# @note Intended for use primarily in testing and debugging.
-
1
class SimpleExecutorService < RubyExecutorService
-
-
# @!macro executor_service_method_post
-
1
def self.post(*args)
-
raise ArgumentError.new('no block given') unless block_given?
-
Thread.new(*args) do
-
Thread.current.abort_on_exception = false
-
yield(*args)
-
end
-
true
-
end
-
-
# @!macro executor_service_method_left_shift
-
1
def self.<<(task)
-
post(&task)
-
self
-
end
-
-
# @!macro executor_service_method_post
-
1
def post(*args, &task)
-
raise ArgumentError.new('no block given') unless block_given?
-
return false unless running?
-
@count.increment
-
Thread.new(*args) do
-
Thread.current.abort_on_exception = false
-
begin
-
yield(*args)
-
ensure
-
@count.decrement
-
@stopped.set if @running.false? && @count.value == 0
-
end
-
end
-
end
-
-
# @!macro executor_service_method_left_shift
-
1
def <<(task)
-
post(&task)
-
self
-
end
-
-
# @!macro executor_service_method_running_question
-
1
def running?
-
@running.true?
-
end
-
-
# @!macro executor_service_method_shuttingdown_question
-
1
def shuttingdown?
-
@running.false? && ! @stopped.set?
-
end
-
-
# @!macro executor_service_method_shutdown_question
-
1
def shutdown?
-
@stopped.set?
-
end
-
-
# @!macro executor_service_method_shutdown
-
1
def shutdown
-
@running.make_false
-
@stopped.set if @count.value == 0
-
true
-
end
-
-
# @!macro executor_service_method_kill
-
1
def kill
-
@running.make_false
-
@stopped.set
-
true
-
end
-
-
# @!macro executor_service_method_wait_for_termination
-
1
def wait_for_termination(timeout = nil)
-
@stopped.wait(timeout)
-
end
-
-
1
private
-
-
1
def ns_initialize(*args)
-
@running = Concurrent::AtomicBoolean.new(true)
-
@stopped = Concurrent::Event.new
-
@count = Concurrent::AtomicFixnum.new(0)
-
end
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/executor/ruby_single_thread_executor'
-
-
1
module Concurrent
-
-
1
if Concurrent.on_jruby?
-
require 'concurrent/executor/java_single_thread_executor'
-
end
-
-
SingleThreadExecutorImplementation = case
-
1
when Concurrent.on_jruby?
-
JavaSingleThreadExecutor
-
else
-
1
RubySingleThreadExecutor
-
end
-
1
private_constant :SingleThreadExecutorImplementation
-
-
# @!macro single_thread_executor
-
#
-
# A thread pool with a single thread an unlimited queue. Should the thread
-
# die for any reason it will be removed and replaced, thus ensuring that
-
# the executor will always remain viable and available to process jobs.
-
#
-
# A common pattern for background processing is to create a single thread
-
# on which an infinite loop is run. The thread's loop blocks on an input
-
# source (perhaps blocking I/O or a queue) and processes each input as it
-
# is received. This pattern has several issues. The thread itself is highly
-
# susceptible to errors during processing. Also, the thread itself must be
-
# constantly monitored and restarted should it die. `SingleThreadExecutor`
-
# encapsulates all these bahaviors. The task processor is highly resilient
-
# to errors from within tasks. Also, should the thread die it will
-
# automatically be restarted.
-
#
-
# The API and behavior of this class are based on Java's `SingleThreadExecutor`.
-
#
-
# @!macro abstract_executor_service_public_api
-
1
class SingleThreadExecutor < SingleThreadExecutorImplementation
-
-
# @!macro single_thread_executor_method_initialize
-
#
-
# Create a new thread pool.
-
#
-
# @option opts [Symbol] :fallback_policy (:discard) the policy for handling new
-
# tasks that are received when the queue size has reached
-
# `max_queue` or the executor has shut down
-
#
-
# @raise [ArgumentError] if `:fallback_policy` is not one of the values specified
-
# in `FALLBACK_POLICIES`
-
#
-
# @see http://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Executors.html
-
# @see http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html
-
-
# @!method initialize(opts = {})
-
# @!macro single_thread_executor_method_initialize
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/executor/ruby_thread_pool_executor'
-
-
1
module Concurrent
-
-
1
if Concurrent.on_jruby?
-
require 'concurrent/executor/java_thread_pool_executor'
-
end
-
-
ThreadPoolExecutorImplementation = case
-
1
when Concurrent.on_jruby?
-
JavaThreadPoolExecutor
-
else
-
1
RubyThreadPoolExecutor
-
end
-
1
private_constant :ThreadPoolExecutorImplementation
-
-
# @!macro thread_pool_executor
-
#
-
# An abstraction composed of one or more threads and a task queue. Tasks
-
# (blocks or `proc` objects) are submitted to the pool and added to the queue.
-
# The threads in the pool remove the tasks and execute them in the order
-
# they were received.
-
#
-
# A `ThreadPoolExecutor` will automatically adjust the pool size according
-
# to the bounds set by `min-threads` and `max-threads`. When a new task is
-
# submitted and fewer than `min-threads` threads are running, a new thread
-
# is created to handle the request, even if other worker threads are idle.
-
# If there are more than `min-threads` but less than `max-threads` threads
-
# running, a new thread will be created only if the queue is full.
-
#
-
# Threads that are idle for too long will be garbage collected, down to the
-
# configured minimum options. Should a thread crash it, too, will be garbage collected.
-
#
-
# `ThreadPoolExecutor` is based on the Java class of the same name. From
-
# the official Java documentation;
-
#
-
# > Thread pools address two different problems: they usually provide
-
# > improved performance when executing large numbers of asynchronous tasks,
-
# > due to reduced per-task invocation overhead, and they provide a means
-
# > of bounding and managing the resources, including threads, consumed
-
# > when executing a collection of tasks. Each ThreadPoolExecutor also
-
# > maintains some basic statistics, such as the number of completed tasks.
-
# >
-
# > To be useful across a wide range of contexts, this class provides many
-
# > adjustable parameters and extensibility hooks. However, programmers are
-
# > urged to use the more convenient Executors factory methods
-
# > [CachedThreadPool] (unbounded thread pool, with automatic thread reclamation),
-
# > [FixedThreadPool] (fixed size thread pool) and [SingleThreadExecutor] (single
-
# > background thread), that preconfigure settings for the most common usage
-
# > scenarios.
-
#
-
# @!macro thread_pool_options
-
#
-
# @!macro thread_pool_executor_public_api
-
1
class ThreadPoolExecutor < ThreadPoolExecutorImplementation
-
-
# @!macro thread_pool_executor_method_initialize
-
#
-
# Create a new thread pool.
-
#
-
# @param [Hash] opts the options which configure the thread pool.
-
#
-
# @option opts [Integer] :max_threads (DEFAULT_MAX_POOL_SIZE) the maximum
-
# number of threads to be created
-
# @option opts [Integer] :min_threads (DEFAULT_MIN_POOL_SIZE) When a new task is submitted
-
# and fewer than `min_threads` are running, a new thread is created
-
# @option opts [Integer] :idletime (DEFAULT_THREAD_IDLETIMEOUT) the maximum
-
# number of seconds a thread may be idle before being reclaimed
-
# @option opts [Integer] :max_queue (DEFAULT_MAX_QUEUE_SIZE) the maximum
-
# number of tasks allowed in the work queue at any one time; a value of
-
# zero means the queue may grow without bound
-
# @option opts [Symbol] :fallback_policy (:abort) the policy for handling new
-
# tasks that are received when the queue size has reached
-
# `max_queue` or the executor has shut down
-
#
-
# @raise [ArgumentError] if `:max_threads` is less than one
-
# @raise [ArgumentError] if `:min_threads` is less than zero
-
# @raise [ArgumentError] if `:fallback_policy` is not one of the values specified
-
# in `FALLBACK_POLICIES`
-
#
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html
-
-
# @!method initialize(opts = {})
-
# @!macro thread_pool_executor_method_initialize
-
end
-
end
-
1
require 'concurrent/scheduled_task'
-
1
require 'concurrent/atomic/event'
-
1
require 'concurrent/collection/non_concurrent_priority_queue'
-
1
require 'concurrent/executor/executor_service'
-
1
require 'concurrent/executor/single_thread_executor'
-
-
1
require 'concurrent/options'
-
-
1
module Concurrent
-
-
# Executes a collection of tasks, each after a given delay. A master task
-
# monitors the set and schedules each task for execution at the appropriate
-
# time. Tasks are run on the global thread pool or on the supplied executor.
-
# Each task is represented as a `ScheduledTask`.
-
#
-
# @see Concurrent::ScheduledTask
-
#
-
# @!macro monotonic_clock_warning
-
1
class TimerSet < RubyExecutorService
-
-
# Create a new set of timed tasks.
-
#
-
# @!macro executor_options
-
#
-
# @param [Hash] opts the options used to specify the executor on which to perform actions
-
# @option opts [Executor] :executor when set use the given `Executor` instance.
-
# Three special values are also supported: `:task` returns the global task pool,
-
# `:operation` returns the global operation pool, and `:immediate` returns a new
-
# `ImmediateExecutor` object.
-
1
def initialize(opts = {})
-
super(opts)
-
end
-
-
# Post a task to be execute run after a given delay (in seconds). If the
-
# delay is less than 1/100th of a second the task will be immediately post
-
# to the executor.
-
#
-
# @param [Float] delay the number of seconds to wait for before executing the task.
-
# @param [Array<Object>] args the arguments passed to the task on execution.
-
#
-
# @yield the task to be performed.
-
#
-
# @return [Concurrent::ScheduledTask, false] IVar representing the task if the post
-
# is successful; false after shutdown.
-
#
-
# @raise [ArgumentError] if the intended execution time is not in the future.
-
# @raise [ArgumentError] if no block is given.
-
1
def post(delay, *args, &task)
-
raise ArgumentError.new('no block given') unless block_given?
-
return false unless running?
-
opts = { executor: @task_executor,
-
args: args,
-
timer_set: self }
-
task = ScheduledTask.execute(delay, opts, &task) # may raise exception
-
task.unscheduled? ? false : task
-
end
-
-
# Begin an immediate shutdown. In-progress tasks will be allowed to
-
# complete but enqueued tasks will be dismissed and no new tasks
-
# will be accepted. Has no additional effect if the thread pool is
-
# not running.
-
1
def kill
-
shutdown
-
end
-
-
1
private :<<
-
-
1
private
-
-
# Initialize the object.
-
#
-
# @param [Hash] opts the options to create the object with.
-
# @!visibility private
-
1
def ns_initialize(opts)
-
@queue = Collection::NonConcurrentPriorityQueue.new(order: :min)
-
@task_executor = Options.executor_from_options(opts) || Concurrent.global_io_executor
-
@timer_executor = SingleThreadExecutor.new
-
@condition = Event.new
-
@ruby_pid = $$ # detects if Ruby has forked
-
end
-
-
# Post the task to the internal queue.
-
#
-
# @note This is intended as a callback method from ScheduledTask
-
# only. It is not intended to be used directly. Post a task
-
# by using the `SchedulesTask#execute` method.
-
#
-
# @!visibility private
-
1
def post_task(task)
-
synchronize { ns_post_task(task) }
-
end
-
-
# @!visibility private
-
1
def ns_post_task(task)
-
return false unless ns_running?
-
ns_reset_if_forked
-
if (task.initial_delay) <= 0.01
-
task.executor.post { task.process_task }
-
else
-
@queue.push(task)
-
# only post the process method when the queue is empty
-
@timer_executor.post(&method(:process_tasks)) if @queue.length == 1
-
@condition.set
-
end
-
true
-
end
-
-
# Remove the given task from the queue.
-
#
-
# @note This is intended as a callback method from `ScheduledTask`
-
# only. It is not intended to be used directly. Cancel a task
-
# by using the `ScheduledTask#cancel` method.
-
#
-
# @!visibility private
-
1
def remove_task(task)
-
synchronize { @queue.delete(task) }
-
end
-
-
# `ExecutorService` callback called during shutdown.
-
#
-
# @!visibility private
-
1
def ns_shutdown_execution
-
ns_reset_if_forked
-
@queue.clear
-
@timer_executor.kill
-
stopped_event.set
-
end
-
-
1
def ns_reset_if_forked
-
if $$ != @ruby_pid
-
@queue.clear
-
@condition.reset
-
@ruby_pid = $$
-
end
-
end
-
-
# Run a loop and execute tasks in the scheduled order and at the approximate
-
# scheduled time. If no tasks remain the thread will exit gracefully so that
-
# garbage collection can occur. If there are no ready tasks it will sleep
-
# for up to 60 seconds waiting for the next scheduled task.
-
#
-
# @!visibility private
-
1
def process_tasks
-
loop do
-
task = synchronize { @condition.reset; @queue.peek }
-
break unless task
-
-
now = Concurrent.monotonic_time
-
diff = task.schedule_time - now
-
-
if diff <= 0
-
# We need to remove the task from the queue before passing
-
# it to the executor, to avoid race conditions where we pass
-
# the peek'ed task to the executor and then pop a different
-
# one that's been added in the meantime.
-
#
-
# Note that there's no race condition between the peek and
-
# this pop - this pop could retrieve a different task from
-
# the peek, but that task would be due to fire now anyway
-
# (because @queue is a priority queue, and this thread is
-
# the only reader, so whatever timer is at the head of the
-
# queue now must have the same pop time, or a closer one, as
-
# when we peeked).
-
task = synchronize { @queue.pop }
-
task.executor.post { task.process_task }
-
else
-
@condition.wait([diff, 60].min)
-
end
-
end
-
end
-
end
-
end
-
1
require 'concurrent/executor/abstract_executor_service'
-
1
require 'concurrent/executor/cached_thread_pool'
-
1
require 'concurrent/executor/executor_service'
-
1
require 'concurrent/executor/fixed_thread_pool'
-
1
require 'concurrent/executor/immediate_executor'
-
1
require 'concurrent/executor/indirect_immediate_executor'
-
1
require 'concurrent/executor/java_executor_service'
-
1
require 'concurrent/executor/java_single_thread_executor'
-
1
require 'concurrent/executor/java_thread_pool_executor'
-
1
require 'concurrent/executor/ruby_executor_service'
-
1
require 'concurrent/executor/ruby_single_thread_executor'
-
1
require 'concurrent/executor/ruby_thread_pool_executor'
-
1
require 'concurrent/executor/cached_thread_pool'
-
1
require 'concurrent/executor/safe_task_executor'
-
1
require 'concurrent/executor/serial_executor_service'
-
1
require 'concurrent/executor/serialized_execution'
-
1
require 'concurrent/executor/serialized_execution_delegator'
-
1
require 'concurrent/executor/single_thread_executor'
-
1
require 'concurrent/executor/thread_pool_executor'
-
1
require 'concurrent/executor/timer_set'
-
1
require 'thread'
-
1
require 'concurrent/constants'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/ivar'
-
1
require 'concurrent/executor/safe_task_executor'
-
-
1
require 'concurrent/options'
-
-
# TODO (pitr-ch 14-Mar-2017): deprecate, Future, Promise, etc.
-
-
-
1
module Concurrent
-
-
# {include:file:docs-source/future.md}
-
#
-
# @!macro copy_options
-
#
-
# @see http://ruby-doc.org/stdlib-2.1.1/libdoc/observer/rdoc/Observable.html Ruby Observable module
-
# @see http://clojuredocs.org/clojure_core/clojure.core/future Clojure's future function
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Future.html java.util.concurrent.Future
-
1
class Future < IVar
-
-
# Create a new `Future` in the `:unscheduled` state.
-
#
-
# @yield the asynchronous operation to perform
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @option opts [object, Array] :args zero or more arguments to be passed the task
-
# block on execution
-
#
-
# @raise [ArgumentError] if no block is given
-
1
def initialize(opts = {}, &block)
-
raise ArgumentError.new('no block given') unless block_given?
-
super(NULL, opts.merge(__task_from_block__: block), &nil)
-
end
-
-
# Execute an `:unscheduled` `Future`. Immediately sets the state to `:pending` and
-
# passes the block to a new thread/thread pool for eventual execution.
-
# Does nothing if the `Future` is in any state other than `:unscheduled`.
-
#
-
# @return [Future] a reference to `self`
-
#
-
# @example Instance and execute in separate steps
-
# future = Concurrent::Future.new{ sleep(1); 42 }
-
# future.state #=> :unscheduled
-
# future.execute
-
# future.state #=> :pending
-
#
-
# @example Instance and execute in one line
-
# future = Concurrent::Future.new{ sleep(1); 42 }.execute
-
# future.state #=> :pending
-
1
def execute
-
if compare_and_set_state(:pending, :unscheduled)
-
@executor.post{ safe_execute(@task, @args) }
-
self
-
end
-
end
-
-
# Create a new `Future` object with the given block, execute it, and return the
-
# `:pending` object.
-
#
-
# @yield the asynchronous operation to perform
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @option opts [object, Array] :args zero or more arguments to be passed the task
-
# block on execution
-
#
-
# @raise [ArgumentError] if no block is given
-
#
-
# @return [Future] the newly created `Future` in the `:pending` state
-
#
-
# @example
-
# future = Concurrent::Future.execute{ sleep(1); 42 }
-
# future.state #=> :pending
-
1
def self.execute(opts = {}, &block)
-
Future.new(opts, &block).execute
-
end
-
-
# @!macro ivar_set_method
-
1
def set(value = NULL, &block)
-
check_for_block_or_value!(block_given?, value)
-
synchronize do
-
if @state != :unscheduled
-
raise MultipleAssignmentError
-
else
-
@task = block || Proc.new { value }
-
end
-
end
-
execute
-
end
-
-
# Attempt to cancel the operation if it has not already processed.
-
# The operation can only be cancelled while still `pending`. It cannot
-
# be cancelled once it has begun processing or has completed.
-
#
-
# @return [Boolean] was the operation successfully cancelled.
-
1
def cancel
-
if compare_and_set_state(:cancelled, :pending)
-
complete(false, nil, CancelledOperationError.new)
-
true
-
else
-
false
-
end
-
end
-
-
# Has the operation been successfully cancelled?
-
#
-
# @return [Boolean]
-
1
def cancelled?
-
state == :cancelled
-
end
-
-
# Wait the given number of seconds for the operation to complete.
-
# On timeout attempt to cancel the operation.
-
#
-
# @param [Numeric] timeout the maximum time in seconds to wait.
-
# @return [Boolean] true if the operation completed before the timeout
-
# else false
-
1
def wait_or_cancel(timeout)
-
wait(timeout)
-
if complete?
-
true
-
else
-
cancel
-
false
-
end
-
end
-
-
1
protected
-
-
1
def ns_initialize(value, opts)
-
super
-
@state = :unscheduled
-
@task = opts[:__task_from_block__]
-
@executor = Options.executor_from_options(opts) || Concurrent.global_io_executor
-
@args = get_arguments_from(opts)
-
end
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/thread_safe/util'
-
-
1
module Concurrent
-
-
# @!macro concurrent_hash
-
#
-
# A thread-safe subclass of Hash. This version locks against the object
-
# itself for every method call, ensuring only one thread can be reading
-
# or writing at a time. This includes iteration methods like `#each`,
-
# which takes the lock repeatedly when reading an item.
-
#
-
# @see http://ruby-doc.org/core-2.2.0/Hash.html Ruby standard library `Hash`
-
-
# @!macro internal_implementation_note
-
HashImplementation = case
-
1
when Concurrent.on_cruby?
-
# Hash is thread-safe in practice because CRuby runs
-
# threads one at a time and does not do context
-
# switching during the execution of C functions.
-
1
::Hash
-
-
when Concurrent.on_jruby?
-
require 'jruby/synchronized'
-
-
class JRubyHash < ::Hash
-
include JRuby::Synchronized
-
end
-
JRubyHash
-
-
when Concurrent.on_rbx?
-
require 'monitor'
-
require 'concurrent/thread_safe/util/data_structures'
-
-
class RbxHash < ::Hash
-
end
-
ThreadSafe::Util.make_synchronized_on_rbx RbxHash
-
RbxHash
-
-
when Concurrent.on_truffleruby?
-
require 'concurrent/thread_safe/util/data_structures'
-
-
class TruffleRubyHash < ::Hash
-
end
-
-
ThreadSafe::Util.make_synchronized_on_truffleruby TruffleRubyHash
-
TruffleRubyHash
-
-
else
-
warn 'Possibly unsupported Ruby implementation'
-
::Hash
-
end
-
1
private_constant :HashImplementation
-
-
# @!macro concurrent_hash
-
1
class Hash < HashImplementation
-
end
-
-
end
-
1
require 'concurrent/synchronization/abstract_struct'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# A thread-safe, immutable variation of Ruby's standard `Struct`.
-
#
-
# @see http://ruby-doc.org/core-2.2.0/Struct.html Ruby standard library `Struct`
-
1
module ImmutableStruct
-
1
include Synchronization::AbstractStruct
-
-
1
def self.included(base)
-
base.safe_initialization!
-
end
-
-
# @!macro struct_values
-
1
def values
-
ns_values
-
end
-
-
1
alias_method :to_a, :values
-
-
# @!macro struct_values_at
-
1
def values_at(*indexes)
-
ns_values_at(indexes)
-
end
-
-
# @!macro struct_inspect
-
1
def inspect
-
ns_inspect
-
end
-
-
1
alias_method :to_s, :inspect
-
-
# @!macro struct_merge
-
1
def merge(other, &block)
-
ns_merge(other, &block)
-
end
-
-
# @!macro struct_to_h
-
1
def to_h
-
ns_to_h
-
end
-
-
# @!macro struct_get
-
1
def [](member)
-
ns_get(member)
-
end
-
-
# @!macro struct_equality
-
1
def ==(other)
-
ns_equality(other)
-
end
-
-
# @!macro struct_each
-
1
def each(&block)
-
return enum_for(:each) unless block_given?
-
ns_each(&block)
-
end
-
-
# @!macro struct_each_pair
-
1
def each_pair(&block)
-
return enum_for(:each_pair) unless block_given?
-
ns_each_pair(&block)
-
end
-
-
# @!macro struct_select
-
1
def select(&block)
-
return enum_for(:select) unless block_given?
-
ns_select(&block)
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def initialize_copy(original)
-
super(original)
-
ns_initialize_copy
-
end
-
-
# @!macro struct_new
-
1
def self.new(*args, &block)
-
clazz_name = nil
-
if args.length == 0
-
raise ArgumentError.new('wrong number of arguments (0 for 1+)')
-
elsif args.length > 0 && args.first.is_a?(String)
-
clazz_name = args.shift
-
end
-
FACTORY.define_struct(clazz_name, args, &block)
-
end
-
-
1
FACTORY = Class.new(Synchronization::LockableObject) do
-
1
def define_struct(name, members, &block)
-
synchronize do
-
Synchronization::AbstractStruct.define_struct_class(ImmutableStruct, Synchronization::Object, name, members, &block)
-
end
-
end
-
end.new
-
1
private_constant :FACTORY
-
end
-
end
-
1
require 'concurrent/constants'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/collection/copy_on_write_observer_set'
-
1
require 'concurrent/concern/obligation'
-
1
require 'concurrent/concern/observable'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# An `IVar` is like a future that you can assign. As a future is a value that
-
# is being computed that you can wait on, an `IVar` is a value that is waiting
-
# to be assigned, that you can wait on. `IVars` are single assignment and
-
# deterministic.
-
#
-
# Then, express futures as an asynchronous computation that assigns an `IVar`.
-
# The `IVar` becomes the primitive on which [futures](Future) and
-
# [dataflow](Dataflow) are built.
-
#
-
# An `IVar` is a single-element container that is normally created empty, and
-
# can only be set once. The I in `IVar` stands for immutable. Reading an
-
# `IVar` normally blocks until it is set. It is safe to set and read an `IVar`
-
# from different threads.
-
#
-
# If you want to have some parallel task set the value in an `IVar`, you want
-
# a `Future`. If you want to create a graph of parallel tasks all executed
-
# when the values they depend on are ready you want `dataflow`. `IVar` is
-
# generally a low-level primitive.
-
#
-
# ## Examples
-
#
-
# Create, set and get an `IVar`
-
#
-
# ```ruby
-
# ivar = Concurrent::IVar.new
-
# ivar.set 14
-
# ivar.value #=> 14
-
# ivar.set 2 # would now be an error
-
# ```
-
#
-
# ## See Also
-
#
-
# 1. For the theory: Arvind, R. Nikhil, and K. Pingali.
-
# [I-Structures: Data structures for parallel computing](http://dl.acm.org/citation.cfm?id=69562).
-
# In Proceedings of Workshop on Graph Reduction, 1986.
-
# 2. For recent application:
-
# [DataDrivenFuture in Habanero Java from Rice](http://www.cs.rice.edu/~vs3/hjlib/doc/edu/rice/hj/api/HjDataDrivenFuture.html).
-
1
class IVar < Synchronization::LockableObject
-
1
include Concern::Obligation
-
1
include Concern::Observable
-
-
# Create a new `IVar` in the `:pending` state with the (optional) initial value.
-
#
-
# @param [Object] value the initial value
-
# @param [Hash] opts the options to create a message with
-
# @option opts [String] :dup_on_deref (false) call `#dup` before returning
-
# the data
-
# @option opts [String] :freeze_on_deref (false) call `#freeze` before
-
# returning the data
-
# @option opts [String] :copy_on_deref (nil) call the given `Proc` passing
-
# the internal value and returning the value returned from the proc
-
1
def initialize(value = NULL, opts = {}, &block)
-
if value != NULL && block_given?
-
raise ArgumentError.new('provide only a value or a block')
-
end
-
super(&nil)
-
synchronize { ns_initialize(value, opts, &block) }
-
end
-
-
# Add an observer on this object that will receive notification on update.
-
#
-
# Upon completion the `IVar` will notify all observers in a thread-safe way.
-
# The `func` method of the observer will be called with three arguments: the
-
# `Time` at which the `Future` completed the asynchronous operation, the
-
# final `value` (or `nil` on rejection), and the final `reason` (or `nil` on
-
# fulfillment).
-
#
-
# @param [Object] observer the object that will be notified of changes
-
# @param [Symbol] func symbol naming the method to call when this
-
# `Observable` has changes`
-
1
def add_observer(observer = nil, func = :update, &block)
-
raise ArgumentError.new('cannot provide both an observer and a block') if observer && block
-
direct_notification = false
-
-
if block
-
observer = block
-
func = :call
-
end
-
-
synchronize do
-
if event.set?
-
direct_notification = true
-
else
-
observers.add_observer(observer, func)
-
end
-
end
-
-
observer.send(func, Time.now, self.value, reason) if direct_notification
-
observer
-
end
-
-
# @!macro ivar_set_method
-
# Set the `IVar` to a value and wake or notify all threads waiting on it.
-
#
-
# @!macro ivar_set_parameters_and_exceptions
-
# @param [Object] value the value to store in the `IVar`
-
# @yield A block operation to use for setting the value
-
# @raise [ArgumentError] if both a value and a block are given
-
# @raise [Concurrent::MultipleAssignmentError] if the `IVar` has already
-
# been set or otherwise completed
-
#
-
# @return [IVar] self
-
1
def set(value = NULL)
-
check_for_block_or_value!(block_given?, value)
-
raise MultipleAssignmentError unless compare_and_set_state(:processing, :pending)
-
-
begin
-
value = yield if block_given?
-
complete_without_notification(true, value, nil)
-
rescue => ex
-
complete_without_notification(false, nil, ex)
-
end
-
-
notify_observers(self.value, reason)
-
self
-
end
-
-
# @!macro ivar_fail_method
-
# Set the `IVar` to failed due to some error and wake or notify all threads waiting on it.
-
#
-
# @param [Object] reason for the failure
-
# @raise [Concurrent::MultipleAssignmentError] if the `IVar` has already
-
# been set or otherwise completed
-
# @return [IVar] self
-
1
def fail(reason = StandardError.new)
-
complete(false, nil, reason)
-
end
-
-
# Attempt to set the `IVar` with the given value or block. Return a
-
# boolean indicating the success or failure of the set operation.
-
#
-
# @!macro ivar_set_parameters_and_exceptions
-
#
-
# @return [Boolean] true if the value was set else false
-
1
def try_set(value = NULL, &block)
-
set(value, &block)
-
true
-
rescue MultipleAssignmentError
-
false
-
end
-
-
1
protected
-
-
# @!visibility private
-
1
def ns_initialize(value, opts)
-
value = yield if block_given?
-
init_obligation
-
self.observers = Collection::CopyOnWriteObserverSet.new
-
set_deref_options(opts)
-
-
@state = :pending
-
if value != NULL
-
ns_complete_without_notification(true, value, nil)
-
end
-
end
-
-
# @!visibility private
-
1
def safe_execute(task, args = [])
-
if compare_and_set_state(:processing, :pending)
-
success, val, reason = SafeTaskExecutor.new(task, rescue_exception: true).execute(*@args)
-
complete(success, val, reason)
-
yield(success, val, reason) if block_given?
-
end
-
end
-
-
# @!visibility private
-
1
def complete(success, value, reason)
-
complete_without_notification(success, value, reason)
-
notify_observers(self.value, reason)
-
self
-
end
-
-
# @!visibility private
-
1
def complete_without_notification(success, value, reason)
-
synchronize { ns_complete_without_notification(success, value, reason) }
-
self
-
end
-
-
# @!visibility private
-
1
def notify_observers(value, reason)
-
observers.notify_and_delete_observers{ [Time.now, value, reason] }
-
end
-
-
# @!visibility private
-
1
def ns_complete_without_notification(success, value, reason)
-
raise MultipleAssignmentError if [:fulfilled, :rejected].include? @state
-
set_state(success, value, reason)
-
event.set
-
end
-
-
# @!visibility private
-
1
def check_for_block_or_value!(block_given, value) # :nodoc:
-
if (block_given && value != NULL) || (! block_given && value == NULL)
-
raise ArgumentError.new('must set with either a value or a block')
-
end
-
end
-
end
-
end
-
1
require 'thread'
-
1
require 'concurrent/constants'
-
1
require 'concurrent/synchronization'
-
1
require 'concurrent/utility/engine'
-
-
1
module Concurrent
-
# @!visibility private
-
1
module Collection
-
-
# @!visibility private
-
MapImplementation = case
-
1
when Concurrent.on_jruby?
-
# noinspection RubyResolve
-
JRubyMapBackend
-
when Concurrent.on_cruby?
-
1
require 'concurrent/collection/map/mri_map_backend'
-
1
MriMapBackend
-
when Concurrent.on_rbx? || Concurrent.on_truffleruby?
-
require 'concurrent/collection/map/atomic_reference_map_backend'
-
AtomicReferenceMapBackend
-
else
-
warn 'Concurrent::Map: unsupported Ruby engine, using a fully synchronized Concurrent::Map implementation'
-
require 'concurrent/collection/map/synchronized_map_backend'
-
SynchronizedMapBackend
-
end
-
end
-
-
# `Concurrent::Map` is a hash-like object and should have much better performance
-
# characteristics, especially under high concurrency, than `Concurrent::Hash`.
-
# However, `Concurrent::Map `is not strictly semantically equivalent to a ruby `Hash`
-
# -- for instance, it does not necessarily retain ordering by insertion time as `Hash`
-
# does. For most uses it should do fine though, and we recommend you consider
-
# `Concurrent::Map` instead of `Concurrent::Hash` for your concurrency-safe hash needs.
-
1
class Map < Collection::MapImplementation
-
-
# @!macro map.atomic_method
-
# This method is atomic.
-
-
# @!macro map.atomic_method_with_block
-
# This method is atomic.
-
# @note Atomic methods taking a block do not allow the `self` instance
-
# to be used within the block. Doing so will cause a deadlock.
-
-
# @!method compute_if_absent(key)
-
# Compute and store new value for key if the key is absent.
-
# @param [Object] key
-
# @yield new value
-
# @yieldreturn [Object] new value
-
# @return [Object] new value or current value
-
# @!macro map.atomic_method_with_block
-
-
# @!method compute_if_present(key)
-
# Compute and store new value for key if the key is present.
-
# @param [Object] key
-
# @yield new value
-
# @yieldparam old_value [Object]
-
# @yieldreturn [Object, nil] new value, when nil the key is removed
-
# @return [Object, nil] new value or nil
-
# @!macro map.atomic_method_with_block
-
-
# @!method compute(key)
-
# Compute and store new value for key.
-
# @param [Object] key
-
# @yield compute new value from old one
-
# @yieldparam old_value [Object, nil] old_value, or nil when key is absent
-
# @yieldreturn [Object, nil] new value, when nil the key is removed
-
# @return [Object, nil] new value or nil
-
# @!macro map.atomic_method_with_block
-
-
# @!method merge_pair(key, value)
-
# If the key is absent, the value is stored, otherwise new value is
-
# computed with a block.
-
# @param [Object] key
-
# @param [Object] value
-
# @yield compute new value from old one
-
# @yieldparam old_value [Object] old value
-
# @yieldreturn [Object, nil] new value, when nil the key is removed
-
# @return [Object, nil] new value or nil
-
# @!macro map.atomic_method_with_block
-
-
# @!method replace_pair(key, old_value, new_value)
-
# Replaces old_value with new_value if key exists and current value
-
# matches old_value
-
# @param [Object] key
-
# @param [Object] old_value
-
# @param [Object] new_value
-
# @return [true, false] true if replaced
-
# @!macro map.atomic_method
-
-
# @!method replace_if_exists(key, new_value)
-
# Replaces current value with new_value if key exists
-
# @param [Object] key
-
# @param [Object] new_value
-
# @return [Object, nil] old value or nil
-
# @!macro map.atomic_method
-
-
# @!method get_and_set(key, value)
-
# Get the current value under key and set new value.
-
# @param [Object] key
-
# @param [Object] value
-
# @return [Object, nil] old value or nil when the key was absent
-
# @!macro map.atomic_method
-
-
# @!method delete(key)
-
# Delete key and its value.
-
# @param [Object] key
-
# @return [Object, nil] old value or nil when the key was absent
-
# @!macro map.atomic_method
-
-
# @!method delete_pair(key, value)
-
# Delete pair and its value if current value equals the provided value.
-
# @param [Object] key
-
# @param [Object] value
-
# @return [true, false] true if deleted
-
# @!macro map.atomic_method
-
-
-
1
def initialize(options = nil, &block)
-
if options.kind_of?(::Hash)
-
validate_options_hash!(options)
-
else
-
options = nil
-
end
-
-
super(options)
-
@default_proc = block
-
end
-
-
# Get a value with key
-
# @param [Object] key
-
# @return [Object] the value
-
1
def [](key)
-
if value = super # non-falsy value is an existing mapping, return it right away
-
value
-
# re-check is done with get_or_default(key, NULL) instead of a simple !key?(key) in order to avoid a race condition, whereby by the time the current thread gets to the key?(key) call
-
# a key => value mapping might have already been created by a different thread (key?(key) would then return true, this elsif branch wouldn't be taken and an incorrent +nil+ value
-
# would be returned)
-
# note: nil == value check is not technically necessary
-
elsif @default_proc && nil == value && NULL == (value = get_or_default(key, NULL))
-
@default_proc.call(self, key)
-
else
-
value
-
end
-
end
-
-
1
alias_method :get, :[]
-
# TODO (pitr-ch 30-Oct-2018): doc
-
1
alias_method :put, :[]=
-
-
# Get a value with key, or default_value when key is absent,
-
# or fail when no default_value is given.
-
# @param [Object] key
-
# @param [Object] default_value
-
# @yield default value for a key
-
# @yieldparam key [Object]
-
# @yieldreturn [Object] default value
-
# @return [Object] the value or default value
-
# @raise [KeyError] when key is missing and no default_value is provided
-
# @!macro map_method_not_atomic
-
# @note The "fetch-then-act" methods of `Map` are not atomic. `Map` is intended
-
# to be use as a concurrency primitive with strong happens-before
-
# guarantees. It is not intended to be used as a high-level abstraction
-
# supporting complex operations. All read and write operations are
-
# thread safe, but no guarantees are made regarding race conditions
-
# between the fetch operation and yielding to the block. Additionally,
-
# this method does not support recursion. This is due to internal
-
# constraints that are very unlikely to change in the near future.
-
1
def fetch(key, default_value = NULL)
-
if NULL != (value = get_or_default(key, NULL))
-
value
-
elsif block_given?
-
yield key
-
elsif NULL != default_value
-
default_value
-
else
-
raise_fetch_no_key
-
end
-
end
-
-
# Fetch value with key, or store default value when key is absent,
-
# or fail when no default_value is given. This is a two step operation,
-
# therefore not atomic. The store can overwrite other concurrently
-
# stored value.
-
# @param [Object] key
-
# @param [Object] default_value
-
# @yield default value for a key
-
# @yieldparam key [Object]
-
# @yieldreturn [Object] default value
-
# @return [Object] the value or default value
-
# @!macro map.atomic_method_with_block
-
1
def fetch_or_store(key, default_value = NULL)
-
fetch(key) do
-
put(key, block_given? ? yield(key) : (NULL == default_value ? raise_fetch_no_key : default_value))
-
end
-
end
-
-
# Insert value into map with key if key is absent in one atomic step.
-
# @param [Object] key
-
# @param [Object] value
-
# @return [Object, nil] the previous value when key was present or nil when there was no key
-
def put_if_absent(key, value)
-
computed = false
-
result = compute_if_absent(key) do
-
computed = true
-
value
-
end
-
computed ? nil : result
-
1
end unless method_defined?(:put_if_absent)
-
-
# Is the value stored in the map. Iterates over all values.
-
# @param [Object] value
-
# @return [true, false]
-
1
def value?(value)
-
each_value do |v|
-
return true if value.equal?(v)
-
end
-
false
-
end
-
-
# All keys
-
# @return [::Array<Object>] keys
-
def keys
-
arr = []
-
each_pair { |k, v| arr << k }
-
arr
-
1
end unless method_defined?(:keys)
-
-
# All values
-
# @return [::Array<Object>] values
-
def values
-
arr = []
-
each_pair { |k, v| arr << v }
-
arr
-
1
end unless method_defined?(:values)
-
-
# Iterates over each key.
-
# @yield for each key in the map
-
# @yieldparam key [Object]
-
# @return [self]
-
# @!macro map.atomic_method_with_block
-
def each_key
-
each_pair { |k, v| yield k }
-
1
end unless method_defined?(:each_key)
-
-
# Iterates over each value.
-
# @yield for each value in the map
-
# @yieldparam value [Object]
-
# @return [self]
-
# @!macro map.atomic_method_with_block
-
def each_value
-
each_pair { |k, v| yield v }
-
1
end unless method_defined?(:each_value)
-
-
# Iterates over each key value pair.
-
# @yield for each key value pair in the map
-
# @yieldparam key [Object]
-
# @yieldparam value [Object]
-
# @return [self]
-
# @!macro map.atomic_method_with_block
-
1
def each_pair
-
return enum_for :each_pair unless block_given?
-
super
-
end
-
-
1
alias_method :each, :each_pair unless method_defined?(:each)
-
-
# Find key of a value.
-
# @param [Object] value
-
# @return [Object, nil] key or nil when not found
-
def key(value)
-
each_pair { |k, v| return k if v == value }
-
nil
-
1
end unless method_defined?(:key)
-
1
alias_method :index, :key if RUBY_VERSION < '1.9'
-
-
# Is map empty?
-
# @return [true, false]
-
def empty?
-
each_pair { |k, v| return false }
-
true
-
1
end unless method_defined?(:empty?)
-
-
# The size of map.
-
# @return [Integer] size
-
def size
-
count = 0
-
each_pair { |k, v| count += 1 }
-
count
-
1
end unless method_defined?(:size)
-
-
# @!visibility private
-
1
def marshal_dump
-
raise TypeError, "can't dump hash with default proc" if @default_proc
-
h = {}
-
each_pair { |k, v| h[k] = v }
-
h
-
end
-
-
# @!visibility private
-
1
def marshal_load(hash)
-
initialize
-
populate_from(hash)
-
end
-
-
1
undef :freeze
-
-
# @!visibility private
-
1
def inspect
-
format '%s entries=%d default_proc=%s>', to_s[0..-2], size.to_s, @default_proc.inspect
-
end
-
-
1
private
-
-
1
def raise_fetch_no_key
-
raise KeyError, 'key not found'
-
end
-
-
1
def initialize_copy(other)
-
super
-
populate_from(other)
-
end
-
-
1
def populate_from(hash)
-
hash.each_pair { |k, v| self[k] = v }
-
self
-
end
-
-
1
def validate_options_hash!(options)
-
if (initial_capacity = options[:initial_capacity]) && (!initial_capacity.kind_of?(Integer) || initial_capacity < 0)
-
raise ArgumentError, ":initial_capacity must be a positive Integer"
-
end
-
if (load_factor = options[:load_factor]) && (!load_factor.kind_of?(Numeric) || load_factor <= 0 || load_factor > 1)
-
raise ArgumentError, ":load_factor must be a number between 0 and 1"
-
end
-
end
-
end
-
end
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# A `Maybe` encapsulates an optional value. A `Maybe` either contains a value
-
# of (represented as `Just`), or it is empty (represented as `Nothing`). Using
-
# `Maybe` is a good way to deal with errors or exceptional cases without
-
# resorting to drastic measures such as exceptions.
-
#
-
# `Maybe` is a replacement for the use of `nil` with better type checking.
-
#
-
# For compatibility with {Concurrent::Concern::Obligation} the predicate and
-
# accessor methods are aliased as `fulfilled?`, `rejected?`, `value`, and
-
# `reason`.
-
#
-
# ## Motivation
-
#
-
# A common pattern in languages with pattern matching, such as Erlang and
-
# Haskell, is to return *either* a value *or* an error from a function
-
# Consider this Erlang code:
-
#
-
# ```erlang
-
# case file:consult("data.dat") of
-
# {ok, Terms} -> do_something_useful(Terms);
-
# {error, Reason} -> lager:error(Reason)
-
# end.
-
# ```
-
#
-
# In this example the standard library function `file:consult` returns a
-
# [tuple](http://erlang.org/doc/reference_manual/data_types.html#id69044)
-
# with two elements: an [atom](http://erlang.org/doc/reference_manual/data_types.html#id64134)
-
# (similar to a ruby symbol) and a variable containing ancillary data. On
-
# success it returns the atom `ok` and the data from the file. On failure it
-
# returns `error` and a string with an explanation of the problem. With this
-
# pattern there is no ambiguity regarding success or failure. If the file is
-
# empty the return value cannot be misinterpreted as an error. And when an
-
# error occurs the return value provides useful information.
-
#
-
# In Ruby we tend to return `nil` when an error occurs or else we raise an
-
# exception. Both of these idioms are problematic. Returning `nil` is
-
# ambiguous because `nil` may also be a valid value. It also lacks
-
# information pertaining to the nature of the error. Raising an exception
-
# is both expensive and usurps the normal flow of control. All of these
-
# problems can be solved with the use of a `Maybe`.
-
#
-
# A `Maybe` is unambiguous with regard to whether or not it contains a value.
-
# When `Just` it contains a value, when `Nothing` it does not. When `Just`
-
# the value it contains may be `nil`, which is perfectly valid. When
-
# `Nothing` the reason for the lack of a value is contained as well. The
-
# previous Erlang example can be duplicated in Ruby in a principled way by
-
# having functions return `Maybe` objects:
-
#
-
# ```ruby
-
# result = MyFileUtils.consult("data.dat") # returns a Maybe
-
# if result.just?
-
# do_something_useful(result.value) # or result.just
-
# else
-
# logger.error(result.reason) # or result.nothing
-
# end
-
# ```
-
#
-
# @example Returning a Maybe from a Function
-
# module MyFileUtils
-
# def self.consult(path)
-
# file = File.open(path, 'r')
-
# Concurrent::Maybe.just(file.read)
-
# rescue => ex
-
# return Concurrent::Maybe.nothing(ex)
-
# ensure
-
# file.close if file
-
# end
-
# end
-
#
-
# maybe = MyFileUtils.consult('bogus.file')
-
# maybe.just? #=> false
-
# maybe.nothing? #=> true
-
# maybe.reason #=> #<Errno::ENOENT: No such file or directory @ rb_sysopen - bogus.file>
-
#
-
# maybe = MyFileUtils.consult('README.md')
-
# maybe.just? #=> true
-
# maybe.nothing? #=> false
-
# maybe.value #=> "# Concurrent Ruby\n[![Gem Version..."
-
#
-
# @example Using Maybe with a Block
-
# result = Concurrent::Maybe.from do
-
# Client.find(10) # Client is an ActiveRecord model
-
# end
-
#
-
# # -- if the record was found
-
# result.just? #=> true
-
# result.value #=> #<Client id: 10, first_name: "Ryan">
-
#
-
# # -- if the record was not found
-
# result.just? #=> false
-
# result.reason #=> ActiveRecord::RecordNotFound
-
#
-
# @example Using Maybe with the Null Object Pattern
-
# # In a Rails controller...
-
# result = ClientService.new(10).find # returns a Maybe
-
# render json: result.or(NullClient.new)
-
#
-
# @see https://hackage.haskell.org/package/base-4.2.0.1/docs/Data-Maybe.html Haskell Data.Maybe
-
# @see https://github.com/purescript/purescript-maybe/blob/master/docs/Data.Maybe.md PureScript Data.Maybe
-
1
class Maybe < Synchronization::Object
-
1
include Comparable
-
1
safe_initialization!
-
-
# Indicates that the given attribute has not been set.
-
# When `Just` the {#nothing} getter will return `NONE`.
-
# When `Nothing` the {#just} getter will return `NONE`.
-
1
NONE = ::Object.new.freeze
-
-
# The value of a `Maybe` when `Just`. Will be `NONE` when `Nothing`.
-
1
attr_reader :just
-
-
# The reason for the `Maybe` when `Nothing`. Will be `NONE` when `Just`.
-
1
attr_reader :nothing
-
-
1
private_class_method :new
-
-
# Create a new `Maybe` using the given block.
-
#
-
# Runs the given block passing all function arguments to the block as block
-
# arguments. If the block runs to completion without raising an exception
-
# a new `Just` is created with the value set to the return value of the
-
# block. If the block raises an exception a new `Nothing` is created with
-
# the reason being set to the raised exception.
-
#
-
# @param [Array<Object>] args Zero or more arguments to pass to the block.
-
# @yield The block from which to create a new `Maybe`.
-
# @yieldparam [Array<Object>] args Zero or more block arguments passed as
-
# arguments to the function.
-
#
-
# @return [Maybe] The newly created object.
-
#
-
# @raise [ArgumentError] when no block given.
-
1
def self.from(*args)
-
raise ArgumentError.new('no block given') unless block_given?
-
begin
-
value = yield(*args)
-
return new(value, NONE)
-
rescue => ex
-
return new(NONE, ex)
-
end
-
end
-
-
# Create a new `Just` with the given value.
-
#
-
# @param [Object] value The value to set for the new `Maybe` object.
-
#
-
# @return [Maybe] The newly created object.
-
1
def self.just(value)
-
return new(value, NONE)
-
end
-
-
# Create a new `Nothing` with the given (optional) reason.
-
#
-
# @param [Exception] error The reason to set for the new `Maybe` object.
-
# When given a string a new `StandardError` will be created with the
-
# argument as the message. When no argument is given a new
-
# `StandardError` with an empty message will be created.
-
#
-
# @return [Maybe] The newly created object.
-
1
def self.nothing(error = '')
-
if error.is_a?(Exception)
-
nothing = error
-
else
-
nothing = StandardError.new(error.to_s)
-
end
-
return new(NONE, nothing)
-
end
-
-
# Is this `Maybe` a `Just` (successfully fulfilled with a value)?
-
#
-
# @return [Boolean] True if `Just` or false if `Nothing`.
-
1
def just?
-
! nothing?
-
end
-
1
alias :fulfilled? :just?
-
-
# Is this `Maybe` a `nothing` (rejected with an exception upon fulfillment)?
-
#
-
# @return [Boolean] True if `Nothing` or false if `Just`.
-
1
def nothing?
-
@nothing != NONE
-
end
-
1
alias :rejected? :nothing?
-
-
1
alias :value :just
-
-
1
alias :reason :nothing
-
-
# Comparison operator.
-
#
-
# @return [Integer] 0 if self and other are both `Nothing`;
-
# -1 if self is `Nothing` and other is `Just`;
-
# 1 if self is `Just` and other is nothing;
-
# `self.just <=> other.just` if both self and other are `Just`.
-
1
def <=>(other)
-
if nothing?
-
other.nothing? ? 0 : -1
-
else
-
other.nothing? ? 1 : just <=> other.just
-
end
-
end
-
-
# Return either the value of self or the given default value.
-
#
-
# @return [Object] The value of self when `Just`; else the given default.
-
1
def or(other)
-
just? ? just : other
-
end
-
-
1
private
-
-
# Create a new `Maybe` with the given attributes.
-
#
-
# @param [Object] just The value when `Just` else `NONE`.
-
# @param [Exception, Object] nothing The exception when `Nothing` else `NONE`.
-
#
-
# @return [Maybe] The new `Maybe`.
-
#
-
# @!visibility private
-
1
def initialize(just, nothing)
-
@just = just
-
@nothing = nothing
-
end
-
end
-
end
-
1
require 'concurrent/synchronization/abstract_struct'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# An thread-safe variation of Ruby's standard `Struct`. Values can be set at
-
# construction or safely changed at any time during the object's lifecycle.
-
#
-
# @see http://ruby-doc.org/core-2.2.0/Struct.html Ruby standard library `Struct`
-
1
module MutableStruct
-
1
include Synchronization::AbstractStruct
-
-
# @!macro struct_new
-
#
-
# Factory for creating new struct classes.
-
#
-
# ```
-
# new([class_name] [, member_name]+>) -> StructClass click to toggle source
-
# new([class_name] [, member_name]+>) {|StructClass| block } -> StructClass
-
# new(value, ...) -> obj
-
# StructClass[value, ...] -> obj
-
# ```
-
#
-
# The first two forms are used to create a new struct subclass `class_name`
-
# that can contain a value for each member_name . This subclass can be
-
# used to create instances of the structure like any other Class .
-
#
-
# If the `class_name` is omitted an anonymous struct class will be created.
-
# Otherwise, the name of this struct will appear as a constant in the struct class,
-
# so it must be unique for all structs under this base class and must start with a
-
# capital letter. Assigning a struct class to a constant also gives the class
-
# the name of the constant.
-
#
-
# If a block is given it will be evaluated in the context of `StructClass`, passing
-
# the created class as a parameter. This is the recommended way to customize a struct.
-
# Subclassing an anonymous struct creates an extra anonymous class that will never be used.
-
#
-
# The last two forms create a new instance of a struct subclass. The number of value
-
# parameters must be less than or equal to the number of attributes defined for the
-
# struct. Unset parameters default to nil. Passing more parameters than number of attributes
-
# will raise an `ArgumentError`.
-
#
-
# @see http://ruby-doc.org/core-2.2.0/Struct.html#method-c-new Ruby standard library `Struct#new`
-
-
# @!macro struct_values
-
#
-
# Returns the values for this struct as an Array.
-
#
-
# @return [Array] the values for this struct
-
#
-
1
def values
-
synchronize { ns_values }
-
end
-
1
alias_method :to_a, :values
-
-
# @!macro struct_values_at
-
#
-
# Returns the struct member values for each selector as an Array.
-
#
-
# A selector may be either an Integer offset or a Range of offsets (as in `Array#values_at`).
-
#
-
# @param [Fixnum, Range] indexes the index(es) from which to obatin the values (in order)
-
1
def values_at(*indexes)
-
synchronize { ns_values_at(indexes) }
-
end
-
-
# @!macro struct_inspect
-
#
-
# Describe the contents of this struct in a string.
-
#
-
# @return [String] the contents of this struct in a string
-
1
def inspect
-
synchronize { ns_inspect }
-
end
-
1
alias_method :to_s, :inspect
-
-
# @!macro struct_merge
-
#
-
# Returns a new struct containing the contents of `other` and the contents
-
# of `self`. If no block is specified, the value for entries with duplicate
-
# keys will be that of `other`. Otherwise the value for each duplicate key
-
# is determined by calling the block with the key, its value in `self` and
-
# its value in `other`.
-
#
-
# @param [Hash] other the hash from which to set the new values
-
# @yield an options block for resolving duplicate keys
-
# @yieldparam [String, Symbol] member the name of the member which is duplicated
-
# @yieldparam [Object] selfvalue the value of the member in `self`
-
# @yieldparam [Object] othervalue the value of the member in `other`
-
#
-
# @return [Synchronization::AbstractStruct] a new struct with the new values
-
#
-
# @raise [ArgumentError] of given a member that is not defined in the struct
-
1
def merge(other, &block)
-
synchronize { ns_merge(other, &block) }
-
end
-
-
# @!macro struct_to_h
-
#
-
# Returns a hash containing the names and values for the struct’s members.
-
#
-
# @return [Hash] the names and values for the struct’s members
-
1
def to_h
-
synchronize { ns_to_h }
-
end
-
-
# @!macro struct_get
-
#
-
# Attribute Reference
-
#
-
# @param [Symbol, String, Integer] member the string or symbol name of the member
-
# for which to obtain the value or the member's index
-
#
-
# @return [Object] the value of the given struct member or the member at the given index.
-
#
-
# @raise [NameError] if the member does not exist
-
# @raise [IndexError] if the index is out of range.
-
1
def [](member)
-
synchronize { ns_get(member) }
-
end
-
-
# @!macro struct_equality
-
#
-
# Equality
-
#
-
# @return [Boolean] true if other has the same struct subclass and has
-
# equal member values (according to `Object#==`)
-
1
def ==(other)
-
synchronize { ns_equality(other) }
-
end
-
-
# @!macro struct_each
-
#
-
# Yields the value of each struct member in order. If no block is given
-
# an enumerator is returned.
-
#
-
# @yield the operation to be performed on each struct member
-
# @yieldparam [Object] value each struct value (in order)
-
1
def each(&block)
-
return enum_for(:each) unless block_given?
-
synchronize { ns_each(&block) }
-
end
-
-
# @!macro struct_each_pair
-
#
-
# Yields the name and value of each struct member in order. If no block is
-
# given an enumerator is returned.
-
#
-
# @yield the operation to be performed on each struct member/value pair
-
# @yieldparam [Object] member each struct member (in order)
-
# @yieldparam [Object] value each struct value (in order)
-
1
def each_pair(&block)
-
return enum_for(:each_pair) unless block_given?
-
synchronize { ns_each_pair(&block) }
-
end
-
-
# @!macro struct_select
-
#
-
# Yields each member value from the struct to the block and returns an Array
-
# containing the member values from the struct for which the given block
-
# returns a true value (equivalent to `Enumerable#select`).
-
#
-
# @yield the operation to be performed on each struct member
-
# @yieldparam [Object] value each struct value (in order)
-
#
-
# @return [Array] an array containing each value for which the block returns true
-
1
def select(&block)
-
return enum_for(:select) unless block_given?
-
synchronize { ns_select(&block) }
-
end
-
-
# @!macro struct_set
-
#
-
# Attribute Assignment
-
#
-
# Sets the value of the given struct member or the member at the given index.
-
#
-
# @param [Symbol, String, Integer] member the string or symbol name of the member
-
# for which to obtain the value or the member's index
-
#
-
# @return [Object] the value of the given struct member or the member at the given index.
-
#
-
# @raise [NameError] if the name does not exist
-
# @raise [IndexError] if the index is out of range.
-
1
def []=(member, value)
-
if member.is_a? Integer
-
length = synchronize { @values.length }
-
if member >= length
-
raise IndexError.new("offset #{member} too large for struct(size:#{length})")
-
end
-
synchronize { @values[member] = value }
-
else
-
send("#{member}=", value)
-
end
-
rescue NoMethodError
-
raise NameError.new("no member '#{member}' in struct")
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def initialize_copy(original)
-
synchronize do
-
super(original)
-
ns_initialize_copy
-
end
-
end
-
-
# @!macro struct_new
-
1
def self.new(*args, &block)
-
clazz_name = nil
-
if args.length == 0
-
raise ArgumentError.new('wrong number of arguments (0 for 1+)')
-
elsif args.length > 0 && args.first.is_a?(String)
-
clazz_name = args.shift
-
end
-
FACTORY.define_struct(clazz_name, args, &block)
-
end
-
-
1
FACTORY = Class.new(Synchronization::LockableObject) do
-
1
def define_struct(name, members, &block)
-
synchronize do
-
clazz = Synchronization::AbstractStruct.define_struct_class(MutableStruct, Synchronization::LockableObject, name, members, &block)
-
members.each_with_index do |member, index|
-
clazz.send :remove_method, member
-
clazz.send(:define_method, member) do
-
synchronize { @values[index] }
-
end
-
clazz.send(:define_method, "#{member}=") do |value|
-
synchronize { @values[index] = value }
-
end
-
end
-
clazz
-
end
-
end
-
end.new
-
1
private_constant :FACTORY
-
end
-
end
-
1
require 'concurrent/concern/dereferenceable'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# An `MVar` is a synchronized single element container. They are empty or
-
# contain one item. Taking a value from an empty `MVar` blocks, as does
-
# putting a value into a full one. You can either think of them as blocking
-
# queue of length one, or a special kind of mutable variable.
-
#
-
# On top of the fundamental `#put` and `#take` operations, we also provide a
-
# `#mutate` that is atomic with respect to operations on the same instance.
-
# These operations all support timeouts.
-
#
-
# We also support non-blocking operations `#try_put!` and `#try_take!`, a
-
# `#set!` that ignores existing values, a `#value` that returns the value
-
# without removing it or returns `MVar::EMPTY`, and a `#modify!` that yields
-
# `MVar::EMPTY` if the `MVar` is empty and can be used to set `MVar::EMPTY`.
-
# You shouldn't use these operations in the first instance.
-
#
-
# `MVar` is a [Dereferenceable](Dereferenceable).
-
#
-
# `MVar` is related to M-structures in Id, `MVar` in Haskell and `SyncVar` in Scala.
-
#
-
# Note that unlike the original Haskell paper, our `#take` is blocking. This is how
-
# Haskell and Scala do it today.
-
#
-
# @!macro copy_options
-
#
-
# ## See Also
-
#
-
# 1. P. Barth, R. Nikhil, and Arvind. [M-Structures: Extending a parallel, non- strict, functional language with state](http://dl.acm.org/citation.cfm?id=652538). In Proceedings of the 5th
-
# ACM Conference on Functional Programming Languages and Computer Architecture (FPCA), 1991.
-
#
-
# 2. S. Peyton Jones, A. Gordon, and S. Finne. [Concurrent Haskell](http://dl.acm.org/citation.cfm?id=237794).
-
# In Proceedings of the 23rd Symposium on Principles of Programming Languages
-
# (PoPL), 1996.
-
1
class MVar < Synchronization::Object
-
1
include Concern::Dereferenceable
-
1
safe_initialization!
-
-
# Unique value that represents that an `MVar` was empty
-
1
EMPTY = ::Object.new
-
-
# Unique value that represents that an `MVar` timed out before it was able
-
# to produce a value.
-
1
TIMEOUT = ::Object.new
-
-
# Create a new `MVar`, either empty or with an initial value.
-
#
-
# @param [Hash] opts the options controlling how the future will be processed
-
#
-
# @!macro deref_options
-
1
def initialize(value = EMPTY, opts = {})
-
@value = value
-
@mutex = Mutex.new
-
@empty_condition = ConditionVariable.new
-
@full_condition = ConditionVariable.new
-
set_deref_options(opts)
-
end
-
-
# Remove the value from an `MVar`, leaving it empty, and blocking if there
-
# isn't a value. A timeout can be set to limit the time spent blocked, in
-
# which case it returns `TIMEOUT` if the time is exceeded.
-
# @return [Object] the value that was taken, or `TIMEOUT`
-
1
def take(timeout = nil)
-
@mutex.synchronize do
-
wait_for_full(timeout)
-
-
# If we timed out we'll still be empty
-
if unlocked_full?
-
value = @value
-
@value = EMPTY
-
@empty_condition.signal
-
apply_deref_options(value)
-
else
-
TIMEOUT
-
end
-
end
-
end
-
-
# acquires lock on the from an `MVAR`, yields the value to provided block,
-
# and release lock. A timeout can be set to limit the time spent blocked,
-
# in which case it returns `TIMEOUT` if the time is exceeded.
-
# @return [Object] the value returned by the block, or `TIMEOUT`
-
1
def borrow(timeout = nil)
-
@mutex.synchronize do
-
wait_for_full(timeout)
-
-
# if we timeoud out we'll still be empty
-
if unlocked_full?
-
yield @value
-
else
-
TIMEOUT
-
end
-
end
-
end
-
-
# Put a value into an `MVar`, blocking if there is already a value until
-
# it is empty. A timeout can be set to limit the time spent blocked, in
-
# which case it returns `TIMEOUT` if the time is exceeded.
-
# @return [Object] the value that was put, or `TIMEOUT`
-
1
def put(value, timeout = nil)
-
@mutex.synchronize do
-
wait_for_empty(timeout)
-
-
# If we timed out we won't be empty
-
if unlocked_empty?
-
@value = value
-
@full_condition.signal
-
apply_deref_options(value)
-
else
-
TIMEOUT
-
end
-
end
-
end
-
-
# Atomically `take`, yield the value to a block for transformation, and then
-
# `put` the transformed value. Returns the transformed value. A timeout can
-
# be set to limit the time spent blocked, in which case it returns `TIMEOUT`
-
# if the time is exceeded.
-
# @return [Object] the transformed value, or `TIMEOUT`
-
1
def modify(timeout = nil)
-
raise ArgumentError.new('no block given') unless block_given?
-
-
@mutex.synchronize do
-
wait_for_full(timeout)
-
-
# If we timed out we'll still be empty
-
if unlocked_full?
-
value = @value
-
@value = yield value
-
@full_condition.signal
-
apply_deref_options(value)
-
else
-
TIMEOUT
-
end
-
end
-
end
-
-
# Non-blocking version of `take`, that returns `EMPTY` instead of blocking.
-
1
def try_take!
-
@mutex.synchronize do
-
if unlocked_full?
-
value = @value
-
@value = EMPTY
-
@empty_condition.signal
-
apply_deref_options(value)
-
else
-
EMPTY
-
end
-
end
-
end
-
-
# Non-blocking version of `put`, that returns whether or not it was successful.
-
1
def try_put!(value)
-
@mutex.synchronize do
-
if unlocked_empty?
-
@value = value
-
@full_condition.signal
-
true
-
else
-
false
-
end
-
end
-
end
-
-
# Non-blocking version of `put` that will overwrite an existing value.
-
1
def set!(value)
-
@mutex.synchronize do
-
old_value = @value
-
@value = value
-
@full_condition.signal
-
apply_deref_options(old_value)
-
end
-
end
-
-
# Non-blocking version of `modify` that will yield with `EMPTY` if there is no value yet.
-
1
def modify!
-
raise ArgumentError.new('no block given') unless block_given?
-
-
@mutex.synchronize do
-
value = @value
-
@value = yield value
-
if unlocked_empty?
-
@empty_condition.signal
-
else
-
@full_condition.signal
-
end
-
apply_deref_options(value)
-
end
-
end
-
-
# Returns if the `MVar` is currently empty.
-
1
def empty?
-
@mutex.synchronize { @value == EMPTY }
-
end
-
-
# Returns if the `MVar` currently contains a value.
-
1
def full?
-
!empty?
-
end
-
-
1
protected
-
-
1
def synchronize(&block)
-
@mutex.synchronize(&block)
-
end
-
-
1
private
-
-
1
def unlocked_empty?
-
@value == EMPTY
-
end
-
-
1
def unlocked_full?
-
! unlocked_empty?
-
end
-
-
1
def wait_for_full(timeout)
-
wait_while(@full_condition, timeout) { unlocked_empty? }
-
end
-
-
1
def wait_for_empty(timeout)
-
wait_while(@empty_condition, timeout) { unlocked_full? }
-
end
-
-
1
def wait_while(condition, timeout)
-
if timeout.nil?
-
while yield
-
condition.wait(@mutex)
-
end
-
else
-
stop = Concurrent.monotonic_time + timeout
-
while yield && timeout > 0.0
-
condition.wait(@mutex, timeout)
-
timeout = stop - Concurrent.monotonic_time
-
end
-
end
-
end
-
end
-
end
-
1
require 'concurrent/configuration'
-
-
1
module Concurrent
-
-
# @!visibility private
-
1
module Options
-
-
# Get the requested `Executor` based on the values set in the options hash.
-
#
-
# @param [Hash] opts the options defining the requested executor
-
# @option opts [Executor] :executor when set use the given `Executor` instance.
-
# Three special values are also supported: `:fast` returns the global fast executor,
-
# `:io` returns the global io executor, and `:immediate` returns a new
-
# `ImmediateExecutor` object.
-
#
-
# @return [Executor, nil] the requested thread pool, or nil when no option specified
-
#
-
# @!visibility private
-
1
def self.executor_from_options(opts = {}) # :nodoc:
-
if identifier = opts.fetch(:executor, nil)
-
executor(identifier)
-
else
-
nil
-
end
-
end
-
-
1
def self.executor(executor_identifier)
-
case executor_identifier
-
when :fast
-
Concurrent.global_fast_executor
-
when :io
-
Concurrent.global_io_executor
-
when :immediate
-
Concurrent.global_immediate_executor
-
when Concurrent::ExecutorService
-
executor_identifier
-
else
-
raise ArgumentError, "executor not recognized by '#{executor_identifier}'"
-
end
-
end
-
end
-
end
-
1
require 'thread'
-
1
require 'concurrent/constants'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/ivar'
-
1
require 'concurrent/executor/safe_task_executor'
-
-
1
require 'concurrent/options'
-
-
1
module Concurrent
-
-
1
PromiseExecutionError = Class.new(StandardError)
-
-
# Promises are inspired by the JavaScript [Promises/A](http://wiki.commonjs.org/wiki/Promises/A)
-
# and [Promises/A+](http://promises-aplus.github.io/promises-spec/) specifications.
-
#
-
# > A promise represents the eventual value returned from the single
-
# > completion of an operation.
-
#
-
# Promises are similar to futures and share many of the same behaviours.
-
# Promises are far more robust, however. Promises can be chained in a tree
-
# structure where each promise may have zero or more children. Promises are
-
# chained using the `then` method. The result of a call to `then` is always
-
# another promise. Promises are resolved asynchronously (with respect to the
-
# main thread) but in a strict order: parents are guaranteed to be resolved
-
# before their children, children before their younger siblings. The `then`
-
# method takes two parameters: an optional block to be executed upon parent
-
# resolution and an optional callable to be executed upon parent failure. The
-
# result of each promise is passed to each of its children upon resolution.
-
# When a promise is rejected all its children will be summarily rejected and
-
# will receive the reason.
-
#
-
# Promises have several possible states: *:unscheduled*, *:pending*,
-
# *:processing*, *:rejected*, or *:fulfilled*. These are also aggregated as
-
# `#incomplete?` and `#complete?`. When a Promise is created it is set to
-
# *:unscheduled*. Once the `#execute` method is called the state becomes
-
# *:pending*. Once a job is pulled from the thread pool's queue and is given
-
# to a thread for processing (often immediately upon `#post`) the state
-
# becomes *:processing*. The future will remain in this state until processing
-
# is complete. A future that is in the *:unscheduled*, *:pending*, or
-
# *:processing* is considered `#incomplete?`. A `#complete?` Promise is either
-
# *:rejected*, indicating that an exception was thrown during processing, or
-
# *:fulfilled*, indicating success. If a Promise is *:fulfilled* its `#value`
-
# will be updated to reflect the result of the operation. If *:rejected* the
-
# `reason` will be updated with a reference to the thrown exception. The
-
# predicate methods `#unscheduled?`, `#pending?`, `#rejected?`, and
-
# `#fulfilled?` can be called at any time to obtain the state of the Promise,
-
# as can the `#state` method, which returns a symbol.
-
#
-
# Retrieving the value of a promise is done through the `value` (alias:
-
# `deref`) method. Obtaining the value of a promise is a potentially blocking
-
# operation. When a promise is *rejected* a call to `value` will return `nil`
-
# immediately. When a promise is *fulfilled* a call to `value` will
-
# immediately return the current value. When a promise is *pending* a call to
-
# `value` will block until the promise is either *rejected* or *fulfilled*. A
-
# *timeout* value can be passed to `value` to limit how long the call will
-
# block. If `nil` the call will block indefinitely. If `0` the call will not
-
# block. Any other integer or float value will indicate the maximum number of
-
# seconds to block.
-
#
-
# Promises run on the global thread pool.
-
#
-
# @!macro copy_options
-
#
-
# ### Examples
-
#
-
# Start by requiring promises
-
#
-
# ```ruby
-
# require 'concurrent'
-
# ```
-
#
-
# Then create one
-
#
-
# ```ruby
-
# p = Concurrent::Promise.execute do
-
# # do something
-
# 42
-
# end
-
# ```
-
#
-
# Promises can be chained using the `then` method. The `then` method accepts a
-
# block and an executor, to be executed on fulfillment, and a callable argument to be executed
-
# on rejection. The result of the each promise is passed as the block argument
-
# to chained promises.
-
#
-
# ```ruby
-
# p = Concurrent::Promise.new{10}.then{|x| x * 2}.then{|result| result - 10 }.execute
-
# ```
-
#
-
# And so on, and so on, and so on...
-
#
-
# ```ruby
-
# p = Concurrent::Promise.fulfill(20).
-
# then{|result| result - 10 }.
-
# then{|result| result * 3 }.
-
# then(executor: different_executor){|result| result % 5 }.execute
-
# ```
-
#
-
# The initial state of a newly created Promise depends on the state of its parent:
-
# - if parent is *unscheduled* the child will be *unscheduled*
-
# - if parent is *pending* the child will be *pending*
-
# - if parent is *fulfilled* the child will be *pending*
-
# - if parent is *rejected* the child will be *pending* (but will ultimately be *rejected*)
-
#
-
# Promises are executed asynchronously from the main thread. By the time a
-
# child Promise finishes intialization it may be in a different state than its
-
# parent (by the time a child is created its parent may have completed
-
# execution and changed state). Despite being asynchronous, however, the order
-
# of execution of Promise objects in a chain (or tree) is strictly defined.
-
#
-
# There are multiple ways to create and execute a new `Promise`. Both ways
-
# provide identical behavior:
-
#
-
# ```ruby
-
# # create, operate, then execute
-
# p1 = Concurrent::Promise.new{ "Hello World!" }
-
# p1.state #=> :unscheduled
-
# p1.execute
-
#
-
# # create and immediately execute
-
# p2 = Concurrent::Promise.new{ "Hello World!" }.execute
-
#
-
# # execute during creation
-
# p3 = Concurrent::Promise.execute{ "Hello World!" }
-
# ```
-
#
-
# Once the `execute` method is called a `Promise` becomes `pending`:
-
#
-
# ```ruby
-
# p = Concurrent::Promise.execute{ "Hello, world!" }
-
# p.state #=> :pending
-
# p.pending? #=> true
-
# ```
-
#
-
# Wait a little bit, and the promise will resolve and provide a value:
-
#
-
# ```ruby
-
# p = Concurrent::Promise.execute{ "Hello, world!" }
-
# sleep(0.1)
-
#
-
# p.state #=> :fulfilled
-
# p.fulfilled? #=> true
-
# p.value #=> "Hello, world!"
-
# ```
-
#
-
# If an exception occurs, the promise will be rejected and will provide
-
# a reason for the rejection:
-
#
-
# ```ruby
-
# p = Concurrent::Promise.execute{ raise StandardError.new("Here comes the Boom!") }
-
# sleep(0.1)
-
#
-
# p.state #=> :rejected
-
# p.rejected? #=> true
-
# p.reason #=> "#<StandardError: Here comes the Boom!>"
-
# ```
-
#
-
# #### Rejection
-
#
-
# When a promise is rejected all its children will be rejected and will
-
# receive the rejection `reason` as the rejection callable parameter:
-
#
-
# ```ruby
-
# p = Concurrent::Promise.execute { Thread.pass; raise StandardError }
-
#
-
# c1 = p.then(-> reason { 42 })
-
# c2 = p.then(-> reason { raise 'Boom!' })
-
#
-
# c1.wait.state #=> :fulfilled
-
# c1.value #=> 45
-
# c2.wait.state #=> :rejected
-
# c2.reason #=> #<RuntimeError: Boom!>
-
# ```
-
#
-
# Once a promise is rejected it will continue to accept children that will
-
# receive immediately rejection (they will be executed asynchronously).
-
#
-
# #### Aliases
-
#
-
# The `then` method is the most generic alias: it accepts a block to be
-
# executed upon parent fulfillment and a callable to be executed upon parent
-
# rejection. At least one of them should be passed. The default block is `{
-
# |result| result }` that fulfills the child with the parent value. The
-
# default callable is `{ |reason| raise reason }` that rejects the child with
-
# the parent reason.
-
#
-
# - `on_success { |result| ... }` is the same as `then {|result| ... }`
-
# - `rescue { |reason| ... }` is the same as `then(Proc.new { |reason| ... } )`
-
# - `rescue` is aliased by `catch` and `on_error`
-
1
class Promise < IVar
-
-
# Initialize a new Promise with the provided options.
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @!macro promise_init_options
-
#
-
# @option opts [Promise] :parent the parent `Promise` when building a chain/tree
-
# @option opts [Proc] :on_fulfill fulfillment handler
-
# @option opts [Proc] :on_reject rejection handler
-
# @option opts [object, Array] :args zero or more arguments to be passed
-
# the task block on execution
-
#
-
# @yield The block operation to be performed asynchronously.
-
#
-
# @raise [ArgumentError] if no block is given
-
#
-
# @see http://wiki.commonjs.org/wiki/Promises/A
-
# @see http://promises-aplus.github.io/promises-spec/
-
1
def initialize(opts = {}, &block)
-
opts.delete_if { |k, v| v.nil? }
-
super(NULL, opts.merge(__promise_body_from_block__: block), &nil)
-
end
-
-
# Create a new `Promise` and fulfill it immediately.
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @!macro promise_init_options
-
#
-
# @raise [ArgumentError] if no block is given
-
#
-
# @return [Promise] the newly created `Promise`
-
1
def self.fulfill(value, opts = {})
-
Promise.new(opts).tap { |p| p.send(:synchronized_set_state!, true, value, nil) }
-
end
-
-
# Create a new `Promise` and reject it immediately.
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @!macro promise_init_options
-
#
-
# @raise [ArgumentError] if no block is given
-
#
-
# @return [Promise] the newly created `Promise`
-
1
def self.reject(reason, opts = {})
-
Promise.new(opts).tap { |p| p.send(:synchronized_set_state!, false, nil, reason) }
-
end
-
-
# Execute an `:unscheduled` `Promise`. Immediately sets the state to `:pending` and
-
# passes the block to a new thread/thread pool for eventual execution.
-
# Does nothing if the `Promise` is in any state other than `:unscheduled`.
-
#
-
# @return [Promise] a reference to `self`
-
1
def execute
-
if root?
-
if compare_and_set_state(:pending, :unscheduled)
-
set_pending
-
realize(@promise_body)
-
end
-
else
-
@parent.execute
-
end
-
self
-
end
-
-
# @!macro ivar_set_method
-
#
-
# @raise [Concurrent::PromiseExecutionError] if not the root promise
-
1
def set(value = NULL, &block)
-
raise PromiseExecutionError.new('supported only on root promise') unless root?
-
check_for_block_or_value!(block_given?, value)
-
synchronize do
-
if @state != :unscheduled
-
raise MultipleAssignmentError
-
else
-
@promise_body = block || Proc.new { |result| value }
-
end
-
end
-
execute
-
end
-
-
# @!macro ivar_fail_method
-
#
-
# @raise [Concurrent::PromiseExecutionError] if not the root promise
-
1
def fail(reason = StandardError.new)
-
set { raise reason }
-
end
-
-
# Create a new `Promise` object with the given block, execute it, and return the
-
# `:pending` object.
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @!macro promise_init_options
-
#
-
# @return [Promise] the newly created `Promise` in the `:pending` state
-
#
-
# @raise [ArgumentError] if no block is given
-
#
-
# @example
-
# promise = Concurrent::Promise.execute{ sleep(1); 42 }
-
# promise.state #=> :pending
-
1
def self.execute(opts = {}, &block)
-
new(opts, &block).execute
-
end
-
-
# Chain a new promise off the current promise.
-
#
-
# @return [Promise] the new promise
-
# @yield The block operation to be performed asynchronously.
-
# @overload then(rescuer, executor, &block)
-
# @param [Proc] rescuer An optional rescue block to be executed if the
-
# promise is rejected.
-
# @param [ThreadPool] executor An optional thread pool executor to be used
-
# in the new Promise
-
# @overload then(rescuer, executor: executor, &block)
-
# @param [Proc] rescuer An optional rescue block to be executed if the
-
# promise is rejected.
-
# @param [ThreadPool] executor An optional thread pool executor to be used
-
# in the new Promise
-
1
def then(*args, &block)
-
if args.last.is_a?(::Hash)
-
executor = args.pop[:executor]
-
rescuer = args.first
-
else
-
rescuer, executor = args
-
end
-
-
executor ||= @executor
-
-
raise ArgumentError.new('rescuers and block are both missing') if rescuer.nil? && !block_given?
-
block = Proc.new { |result| result } unless block_given?
-
child = Promise.new(
-
parent: self,
-
executor: executor,
-
on_fulfill: block,
-
on_reject: rescuer
-
)
-
-
synchronize do
-
child.state = :pending if @state == :pending
-
child.on_fulfill(apply_deref_options(@value)) if @state == :fulfilled
-
child.on_reject(@reason) if @state == :rejected
-
@children << child
-
end
-
-
child
-
end
-
-
# Chain onto this promise an action to be undertaken on success
-
# (fulfillment).
-
#
-
# @yield The block to execute
-
#
-
# @return [Promise] self
-
1
def on_success(&block)
-
raise ArgumentError.new('no block given') unless block_given?
-
self.then(&block)
-
end
-
-
# Chain onto this promise an action to be undertaken on failure
-
# (rejection).
-
#
-
# @yield The block to execute
-
#
-
# @return [Promise] self
-
1
def rescue(&block)
-
self.then(block)
-
end
-
-
1
alias_method :catch, :rescue
-
1
alias_method :on_error, :rescue
-
-
# Yield the successful result to the block that returns a promise. If that
-
# promise is also successful the result is the result of the yielded promise.
-
# If either part fails the whole also fails.
-
#
-
# @example
-
# Promise.execute { 1 }.flat_map { |v| Promise.execute { v + 2 } }.value! #=> 3
-
#
-
# @return [Promise]
-
1
def flat_map(&block)
-
child = Promise.new(
-
parent: self,
-
executor: ImmediateExecutor.new,
-
)
-
-
on_error { |e| child.on_reject(e) }
-
on_success do |result1|
-
begin
-
inner = block.call(result1)
-
inner.execute
-
inner.on_success { |result2| child.on_fulfill(result2) }
-
inner.on_error { |e| child.on_reject(e) }
-
rescue => e
-
child.on_reject(e)
-
end
-
end
-
-
child
-
end
-
-
# Builds a promise that produces the result of promises in an Array
-
# and fails if any of them fails.
-
#
-
# @overload zip(*promises)
-
# @param [Array<Promise>] promises
-
#
-
# @overload zip(*promises, opts)
-
# @param [Array<Promise>] promises
-
# @param [Hash] opts the configuration options
-
# @option opts [Executor] :executor (ImmediateExecutor.new) when set use the given `Executor` instance.
-
# @option opts [Boolean] :execute (true) execute promise before returning
-
#
-
# @return [Promise<Array>]
-
1
def self.zip(*promises)
-
opts = promises.last.is_a?(::Hash) ? promises.pop.dup : {}
-
opts[:executor] ||= ImmediateExecutor.new
-
zero = if !opts.key?(:execute) || opts.delete(:execute)
-
fulfill([], opts)
-
else
-
Promise.new(opts) { [] }
-
end
-
-
promises.reduce(zero) do |p1, p2|
-
p1.flat_map do |results|
-
p2.then do |next_result|
-
results << next_result
-
end
-
end
-
end
-
end
-
-
# Builds a promise that produces the result of self and others in an Array
-
# and fails if any of them fails.
-
#
-
# @overload zip(*promises)
-
# @param [Array<Promise>] others
-
#
-
# @overload zip(*promises, opts)
-
# @param [Array<Promise>] others
-
# @param [Hash] opts the configuration options
-
# @option opts [Executor] :executor (ImmediateExecutor.new) when set use the given `Executor` instance.
-
# @option opts [Boolean] :execute (true) execute promise before returning
-
#
-
# @return [Promise<Array>]
-
1
def zip(*others)
-
self.class.zip(self, *others)
-
end
-
-
# Aggregates a collection of promises and executes the `then` condition
-
# if all aggregated promises succeed. Executes the `rescue` handler with
-
# a `Concurrent::PromiseExecutionError` if any of the aggregated promises
-
# fail. Upon execution will execute any of the aggregate promises that
-
# were not already executed.
-
#
-
# @!macro promise_self_aggregate
-
#
-
# The returned promise will not yet have been executed. Additional `#then`
-
# and `#rescue` handlers may still be provided. Once the returned promise
-
# is execute the aggregate promises will be also be executed (if they have
-
# not been executed already). The results of the aggregate promises will
-
# be checked upon completion. The necessary `#then` and `#rescue` blocks
-
# on the aggregating promise will then be executed as appropriate. If the
-
# `#rescue` handlers are executed the raises exception will be
-
# `Concurrent::PromiseExecutionError`.
-
#
-
# @param [Array] promises Zero or more promises to aggregate
-
# @return [Promise] an unscheduled (not executed) promise that aggregates
-
# the promises given as arguments
-
1
def self.all?(*promises)
-
aggregate(:all?, *promises)
-
end
-
-
# Aggregates a collection of promises and executes the `then` condition
-
# if any aggregated promises succeed. Executes the `rescue` handler with
-
# a `Concurrent::PromiseExecutionError` if any of the aggregated promises
-
# fail. Upon execution will execute any of the aggregate promises that
-
# were not already executed.
-
#
-
# @!macro promise_self_aggregate
-
1
def self.any?(*promises)
-
aggregate(:any?, *promises)
-
end
-
-
1
protected
-
-
1
def ns_initialize(value, opts)
-
super
-
-
@executor = Options.executor_from_options(opts) || Concurrent.global_io_executor
-
@args = get_arguments_from(opts)
-
-
@parent = opts.fetch(:parent) { nil }
-
@on_fulfill = opts.fetch(:on_fulfill) { Proc.new { |result| result } }
-
@on_reject = opts.fetch(:on_reject) { Proc.new { |reason| raise reason } }
-
-
@promise_body = opts[:__promise_body_from_block__] || Proc.new { |result| result }
-
@state = :unscheduled
-
@children = []
-
end
-
-
# Aggregate a collection of zero or more promises under a composite promise,
-
# execute the aggregated promises and collect them into a standard Ruby array,
-
# call the given Ruby `Ennnumerable` predicate (such as `any?`, `all?`, `none?`,
-
# or `one?`) on the collection checking for the success or failure of each,
-
# then executing the composite's `#then` handlers if the predicate returns
-
# `true` or executing the composite's `#rescue` handlers if the predicate
-
# returns false.
-
#
-
# @!macro promise_self_aggregate
-
1
def self.aggregate(method, *promises)
-
composite = Promise.new do
-
completed = promises.collect do |promise|
-
promise.execute if promise.unscheduled?
-
promise.wait
-
promise
-
end
-
unless completed.empty? || completed.send(method){|promise| promise.fulfilled? }
-
raise PromiseExecutionError
-
end
-
end
-
composite
-
end
-
-
# @!visibility private
-
1
def set_pending
-
synchronize do
-
@state = :pending
-
@children.each { |c| c.set_pending }
-
end
-
end
-
-
# @!visibility private
-
1
def root? # :nodoc:
-
@parent.nil?
-
end
-
-
# @!visibility private
-
1
def on_fulfill(result)
-
realize Proc.new { @on_fulfill.call(result) }
-
nil
-
end
-
-
# @!visibility private
-
1
def on_reject(reason)
-
realize Proc.new { @on_reject.call(reason) }
-
nil
-
end
-
-
# @!visibility private
-
1
def notify_child(child)
-
if_state(:fulfilled) { child.on_fulfill(apply_deref_options(@value)) }
-
if_state(:rejected) { child.on_reject(@reason) }
-
end
-
-
# @!visibility private
-
1
def complete(success, value, reason)
-
children_to_notify = synchronize do
-
set_state!(success, value, reason)
-
@children.dup
-
end
-
-
children_to_notify.each { |child| notify_child(child) }
-
observers.notify_and_delete_observers{ [Time.now, self.value, reason] }
-
end
-
-
# @!visibility private
-
1
def realize(task)
-
@executor.post do
-
success, value, reason = SafeTaskExecutor.new(task, rescue_exception: true).execute(*@args)
-
complete(success, value, reason)
-
end
-
end
-
-
# @!visibility private
-
1
def set_state!(success, value, reason)
-
set_state(success, value, reason)
-
event.set
-
end
-
-
# @!visibility private
-
1
def synchronized_set_state!(success, value, reason)
-
synchronize { set_state!(success, value, reason) }
-
end
-
end
-
end
-
1
require 'concurrent/synchronization'
-
1
require 'concurrent/atomic/atomic_boolean'
-
1
require 'concurrent/atomic/atomic_fixnum'
-
1
require 'concurrent/collection/lock_free_stack'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/re_include'
-
-
1
module Concurrent
-
-
# {include:file:docs-source/promises-main.md}
-
1
module Promises
-
-
# @!macro promises.param.default_executor
-
# @param [Executor, :io, :fast] default_executor Instance of an executor or a name of the
-
# global executor. Default executor propagates to chained futures unless overridden with
-
# executor parameter or changed with {AbstractEventFuture#with_default_executor}.
-
#
-
# @!macro promises.param.executor
-
# @param [Executor, :io, :fast] executor Instance of an executor or a name of the
-
# global executor. The task is executed on it, default executor remains unchanged.
-
#
-
# @!macro promises.param.args
-
# @param [Object] args arguments which are passed to the task when it's executed.
-
# (It might be prepended with other arguments, see the @yeild section).
-
#
-
# @!macro promises.shortcut.on
-
# Shortcut of {#$0_on} with default `:io` executor supplied.
-
# @see #$0_on
-
#
-
# @!macro promises.shortcut.using
-
# Shortcut of {#$0_using} with default `:io` executor supplied.
-
# @see #$0_using
-
#
-
# @!macro promise.param.task-future
-
# @yieldreturn will become result of the returned Future.
-
# Its returned value becomes {Future#value} fulfilling it,
-
# raised exception becomes {Future#reason} rejecting it.
-
#
-
# @!macro promise.param.callback
-
# @yieldreturn is forgotten.
-
-
# Container of all {Future}, {Event} factory methods. They are never constructed directly with
-
# new.
-
1
module FactoryMethods
-
1
extend ReInclude
-
1
extend self
-
-
1
module Configuration
-
# @return [Executor, :io, :fast] the executor which is used when none is supplied
-
# to a factory method. The method can be overridden in the receivers of
-
# `include FactoryMethod`
-
1
def default_executor
-
:io
-
end
-
end
-
-
1
include Configuration
-
-
# @!macro promises.shortcut.on
-
# @return [ResolvableEvent]
-
1
def resolvable_event
-
resolvable_event_on default_executor
-
end
-
-
# Created resolvable event, user is responsible for resolving the event once by
-
# {Promises::ResolvableEvent#resolve}.
-
#
-
# @!macro promises.param.default_executor
-
# @return [ResolvableEvent]
-
1
def resolvable_event_on(default_executor = self.default_executor)
-
ResolvableEventPromise.new(default_executor).future
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [ResolvableFuture]
-
1
def resolvable_future
-
resolvable_future_on default_executor
-
end
-
-
# Creates resolvable future, user is responsible for resolving the future once by
-
# {Promises::ResolvableFuture#resolve}, {Promises::ResolvableFuture#fulfill},
-
# or {Promises::ResolvableFuture#reject}
-
#
-
# @!macro promises.param.default_executor
-
# @return [ResolvableFuture]
-
1
def resolvable_future_on(default_executor = self.default_executor)
-
ResolvableFuturePromise.new(default_executor).future
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future]
-
1
def future(*args, &task)
-
future_on(default_executor, *args, &task)
-
end
-
-
# Constructs new Future which will be resolved after block is evaluated on default executor.
-
# Evaluation begins immediately.
-
#
-
# @!macro promises.param.default_executor
-
# @!macro promises.param.args
-
# @yield [*args] to the task.
-
# @!macro promise.param.task-future
-
# @return [Future]
-
1
def future_on(default_executor, *args, &task)
-
ImmediateEventPromise.new(default_executor).future.then(*args, &task)
-
end
-
-
# Creates resolved future with will be either fulfilled with the given value or rejection with
-
# the given reason.
-
#
-
# @param [true, false] fulfilled
-
# @param [Object] value
-
# @param [Object] reason
-
# @!macro promises.param.default_executor
-
# @return [Future]
-
1
def resolved_future(fulfilled, value, reason, default_executor = self.default_executor)
-
ImmediateFuturePromise.new(default_executor, fulfilled, value, reason).future
-
end
-
-
# Creates resolved future with will be fulfilled with the given value.
-
#
-
# @!macro promises.param.default_executor
-
# @param [Object] value
-
# @return [Future]
-
1
def fulfilled_future(value, default_executor = self.default_executor)
-
resolved_future true, value, nil, default_executor
-
end
-
-
# Creates resolved future with will be rejected with the given reason.
-
#
-
# @!macro promises.param.default_executor
-
# @param [Object] reason
-
# @return [Future]
-
1
def rejected_future(reason, default_executor = self.default_executor)
-
resolved_future false, nil, reason, default_executor
-
end
-
-
# Creates resolved event.
-
#
-
# @!macro promises.param.default_executor
-
# @return [Event]
-
1
def resolved_event(default_executor = self.default_executor)
-
ImmediateEventPromise.new(default_executor).event
-
end
-
-
# General constructor. Behaves differently based on the argument's type. It's provided for convenience
-
# but it's better to be explicit.
-
#
-
# @see rejected_future, resolved_event, fulfilled_future
-
# @!macro promises.param.default_executor
-
# @return [Event, Future]
-
#
-
# @overload make_future(nil, default_executor = self.default_executor)
-
# @param [nil] nil
-
# @return [Event] resolved event.
-
#
-
# @overload make_future(a_future, default_executor = self.default_executor)
-
# @param [Future] a_future
-
# @return [Future] a future which will be resolved when a_future is.
-
#
-
# @overload make_future(an_event, default_executor = self.default_executor)
-
# @param [Event] an_event
-
# @return [Event] an event which will be resolved when an_event is.
-
#
-
# @overload make_future(exception, default_executor = self.default_executor)
-
# @param [Exception] exception
-
# @return [Future] a rejected future with the exception as its reason.
-
#
-
# @overload make_future(value, default_executor = self.default_executor)
-
# @param [Object] value when none of the above overloads fits
-
# @return [Future] a fulfilled future with the value.
-
1
def make_future(argument = nil, default_executor = self.default_executor)
-
case argument
-
when AbstractEventFuture
-
# returning wrapper would change nothing
-
argument
-
when Exception
-
rejected_future argument, default_executor
-
when nil
-
resolved_event default_executor
-
else
-
fulfilled_future argument, default_executor
-
end
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future, Event]
-
1
def delay(*args, &task)
-
delay_on default_executor, *args, &task
-
end
-
-
# Creates new event or future which is resolved only after it is touched,
-
# see {Concurrent::AbstractEventFuture#touch}.
-
#
-
# @!macro promises.param.default_executor
-
# @overload delay_on(default_executor, *args, &task)
-
# If task is provided it returns a {Future} representing the result of the task.
-
# @!macro promises.param.args
-
# @yield [*args] to the task.
-
# @!macro promise.param.task-future
-
# @return [Future]
-
# @overload delay_on(default_executor)
-
# If no task is provided, it returns an {Event}
-
# @return [Event]
-
1
def delay_on(default_executor, *args, &task)
-
event = DelayPromise.new(default_executor).event
-
task ? event.chain(*args, &task) : event
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future, Event]
-
1
def schedule(intended_time, *args, &task)
-
schedule_on default_executor, intended_time, *args, &task
-
end
-
-
# Creates new event or future which is resolved in intended_time.
-
#
-
# @!macro promises.param.default_executor
-
# @!macro promises.param.intended_time
-
# @param [Numeric, Time] intended_time `Numeric` means to run in `intended_time` seconds.
-
# `Time` means to run on `intended_time`.
-
# @overload schedule_on(default_executor, intended_time, *args, &task)
-
# If task is provided it returns a {Future} representing the result of the task.
-
# @!macro promises.param.args
-
# @yield [*args] to the task.
-
# @!macro promise.param.task-future
-
# @return [Future]
-
# @overload schedule_on(default_executor, intended_time)
-
# If no task is provided, it returns an {Event}
-
# @return [Event]
-
1
def schedule_on(default_executor, intended_time, *args, &task)
-
event = ScheduledPromise.new(default_executor, intended_time).event
-
task ? event.chain(*args, &task) : event
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future]
-
1
def zip_futures(*futures_and_or_events)
-
zip_futures_on default_executor, *futures_and_or_events
-
end
-
-
# Creates new future which is resolved after all futures_and_or_events are resolved.
-
# Its value is array of zipped future values. Its reason is array of reasons for rejection.
-
# If there is an error it rejects.
-
# @!macro promises.event-conversion
-
# If event is supplied, which does not have value and can be only resolved, it's
-
# represented as `:fulfilled` with value `nil`.
-
#
-
# @!macro promises.param.default_executor
-
# @param [AbstractEventFuture] futures_and_or_events
-
# @return [Future]
-
1
def zip_futures_on(default_executor, *futures_and_or_events)
-
ZipFuturesPromise.new_blocked_by(futures_and_or_events, default_executor).future
-
end
-
-
1
alias_method :zip, :zip_futures
-
-
# @!macro promises.shortcut.on
-
# @return [Event]
-
1
def zip_events(*futures_and_or_events)
-
zip_events_on default_executor, *futures_and_or_events
-
end
-
-
# Creates new event which is resolved after all futures_and_or_events are resolved.
-
# (Future is resolved when fulfilled or rejected.)
-
#
-
# @!macro promises.param.default_executor
-
# @param [AbstractEventFuture] futures_and_or_events
-
# @return [Event]
-
1
def zip_events_on(default_executor, *futures_and_or_events)
-
ZipEventsPromise.new_blocked_by(futures_and_or_events, default_executor).event
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future]
-
1
def any_resolved_future(*futures_and_or_events)
-
any_resolved_future_on default_executor, *futures_and_or_events
-
end
-
-
1
alias_method :any, :any_resolved_future
-
-
# Creates new future which is resolved after first futures_and_or_events is resolved.
-
# Its result equals result of the first resolved future.
-
# @!macro promises.any-touch
-
# If resolved it does not propagate {Concurrent::AbstractEventFuture#touch}, leaving delayed
-
# futures un-executed if they are not required any more.
-
# @!macro promises.event-conversion
-
#
-
# @!macro promises.param.default_executor
-
# @param [AbstractEventFuture] futures_and_or_events
-
# @return [Future]
-
1
def any_resolved_future_on(default_executor, *futures_and_or_events)
-
AnyResolvedFuturePromise.new_blocked_by(futures_and_or_events, default_executor).future
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future]
-
1
def any_fulfilled_future(*futures_and_or_events)
-
any_fulfilled_future_on default_executor, *futures_and_or_events
-
end
-
-
# Creates new future which is resolved after first of futures_and_or_events is fulfilled.
-
# Its result equals result of the first resolved future or if all futures_and_or_events reject,
-
# it has reason of the last resolved future.
-
# @!macro promises.any-touch
-
# @!macro promises.event-conversion
-
#
-
# @!macro promises.param.default_executor
-
# @param [AbstractEventFuture] futures_and_or_events
-
# @return [Future]
-
1
def any_fulfilled_future_on(default_executor, *futures_and_or_events)
-
AnyFulfilledFuturePromise.new_blocked_by(futures_and_or_events, default_executor).future
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future]
-
1
def any_event(*futures_and_or_events)
-
any_event_on default_executor, *futures_and_or_events
-
end
-
-
# Creates new event which becomes resolved after first of the futures_and_or_events resolves.
-
# @!macro promises.any-touch
-
#
-
# @!macro promises.param.default_executor
-
# @param [AbstractEventFuture] futures_and_or_events
-
# @return [Event]
-
1
def any_event_on(default_executor, *futures_and_or_events)
-
AnyResolvedEventPromise.new_blocked_by(futures_and_or_events, default_executor).event
-
end
-
-
# TODO consider adding first(count, *futures)
-
# TODO consider adding zip_by(slice, *futures) processing futures in slices
-
# TODO or rather a generic aggregator taking a function
-
end
-
-
1
module InternalStates
-
# @!visibility private
-
1
class State
-
1
def resolved?
-
raise NotImplementedError
-
end
-
-
1
def to_sym
-
raise NotImplementedError
-
end
-
end
-
-
# @!visibility private
-
1
class Pending < State
-
1
def resolved?
-
false
-
end
-
-
1
def to_sym
-
:pending
-
end
-
end
-
-
# @!visibility private
-
1
class Reserved < Pending
-
end
-
-
# @!visibility private
-
1
class ResolvedWithResult < State
-
1
def resolved?
-
true
-
end
-
-
1
def to_sym
-
:resolved
-
end
-
-
1
def result
-
[fulfilled?, value, reason]
-
end
-
-
1
def fulfilled?
-
raise NotImplementedError
-
end
-
-
1
def value
-
raise NotImplementedError
-
end
-
-
1
def reason
-
raise NotImplementedError
-
end
-
-
1
def apply
-
raise NotImplementedError
-
end
-
end
-
-
# @!visibility private
-
1
class Fulfilled < ResolvedWithResult
-
-
1
def initialize(value)
-
1
@Value = value
-
end
-
-
1
def fulfilled?
-
true
-
end
-
-
1
def apply(args, block)
-
block.call value, *args
-
end
-
-
1
def value
-
@Value
-
end
-
-
1
def reason
-
nil
-
end
-
-
1
def to_sym
-
:fulfilled
-
end
-
end
-
-
# @!visibility private
-
1
class FulfilledArray < Fulfilled
-
1
def apply(args, block)
-
block.call(*value, *args)
-
end
-
end
-
-
# @!visibility private
-
1
class Rejected < ResolvedWithResult
-
1
def initialize(reason)
-
@Reason = reason
-
end
-
-
1
def fulfilled?
-
false
-
end
-
-
1
def value
-
nil
-
end
-
-
1
def reason
-
@Reason
-
end
-
-
1
def to_sym
-
:rejected
-
end
-
-
1
def apply(args, block)
-
block.call reason, *args
-
end
-
end
-
-
# @!visibility private
-
1
class PartiallyRejected < ResolvedWithResult
-
1
def initialize(value, reason)
-
super()
-
@Value = value
-
@Reason = reason
-
end
-
-
1
def fulfilled?
-
false
-
end
-
-
1
def to_sym
-
:rejected
-
end
-
-
1
def value
-
@Value
-
end
-
-
1
def reason
-
@Reason
-
end
-
-
1
def apply(args, block)
-
block.call(*reason, *args)
-
end
-
end
-
-
# @!visibility private
-
1
PENDING = Pending.new
-
# @!visibility private
-
1
RESERVED = Reserved.new
-
# @!visibility private
-
1
RESOLVED = Fulfilled.new(nil)
-
-
1
def RESOLVED.to_sym
-
:resolved
-
end
-
end
-
-
1
private_constant :InternalStates
-
-
# @!macro promises.shortcut.event-future
-
# @see Event#$0
-
# @see Future#$0
-
-
# @!macro promises.param.timeout
-
# @param [Numeric] timeout the maximum time in second to wait.
-
-
# @!macro promises.warn.blocks
-
# @note This function potentially blocks current thread until the Future is resolved.
-
# Be careful it can deadlock. Try to chain instead.
-
-
# Common ancestor of {Event} and {Future} classes, many shared methods are defined here.
-
1
class AbstractEventFuture < Synchronization::Object
-
1
safe_initialization!
-
1
attr_atomic(:internal_state)
-
1
private :internal_state=, :swap_internal_state, :compare_and_set_internal_state, :update_internal_state
-
# @!method internal_state
-
# @!visibility private
-
-
1
include InternalStates
-
-
1
def initialize(promise, default_executor)
-
super()
-
@Lock = Mutex.new
-
@Condition = ConditionVariable.new
-
@Promise = promise
-
@DefaultExecutor = default_executor
-
@Callbacks = LockFreeStack.new
-
@Waiters = AtomicFixnum.new 0
-
self.internal_state = PENDING
-
end
-
-
1
private :initialize
-
-
# Returns its state.
-
# @return [Symbol]
-
#
-
# @overload an_event.state
-
# @return [:pending, :resolved]
-
# @overload a_future.state
-
# Both :fulfilled, :rejected implies :resolved.
-
# @return [:pending, :fulfilled, :rejected]
-
1
def state
-
internal_state.to_sym
-
end
-
-
# Is it in pending state?
-
# @return [Boolean]
-
1
def pending?
-
!internal_state.resolved?
-
end
-
-
# Is it in resolved state?
-
# @return [Boolean]
-
1
def resolved?
-
internal_state.resolved?
-
end
-
-
# Propagates touch. Requests all the delayed futures, which it depends on, to be
-
# executed. This method is called by any other method requiring resolved state, like {#wait}.
-
# @return [self]
-
1
def touch
-
@Promise.touch
-
self
-
end
-
-
# @!macro promises.touches
-
# Calls {Concurrent::AbstractEventFuture#touch}.
-
-
# @!macro promises.method.wait
-
# Wait (block the Thread) until receiver is {#resolved?}.
-
# @!macro promises.touches
-
#
-
# @!macro promises.warn.blocks
-
# @!macro promises.param.timeout
-
# @return [self, true, false] self implies timeout was not used, true implies timeout was used
-
# and it was resolved, false implies it was not resolved within timeout.
-
1
def wait(timeout = nil)
-
result = wait_until_resolved(timeout)
-
timeout ? result : self
-
end
-
-
# Returns default executor.
-
# @return [Executor] default executor
-
# @see #with_default_executor
-
# @see FactoryMethods#future_on
-
# @see FactoryMethods#resolvable_future
-
# @see FactoryMethods#any_fulfilled_future_on
-
# @see similar
-
1
def default_executor
-
@DefaultExecutor
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future]
-
1
def chain(*args, &task)
-
chain_on @DefaultExecutor, *args, &task
-
end
-
-
# Chains the task to be executed asynchronously on executor after it is resolved.
-
#
-
# @!macro promises.param.executor
-
# @!macro promises.param.args
-
# @return [Future]
-
# @!macro promise.param.task-future
-
#
-
# @overload an_event.chain_on(executor, *args, &task)
-
# @yield [*args] to the task.
-
# @overload a_future.chain_on(executor, *args, &task)
-
# @yield [fulfilled, value, reason, *args] to the task.
-
# @yieldparam [true, false] fulfilled
-
# @yieldparam [Object] value
-
# @yieldparam [Object] reason
-
1
def chain_on(executor, *args, &task)
-
ChainPromise.new_blocked_by1(self, @DefaultExecutor, executor, args, &task).future
-
end
-
-
# @return [String] Short string representation.
-
1
def to_s
-
format '%s %s>', super[0..-2], state
-
end
-
-
1
alias_method :inspect, :to_s
-
-
# Resolves the resolvable when receiver is resolved.
-
#
-
# @param [Resolvable] resolvable
-
# @return [self]
-
1
def chain_resolvable(resolvable)
-
on_resolution! { resolvable.resolve_with internal_state }
-
end
-
-
1
alias_method :tangle, :chain_resolvable
-
-
# @!macro promises.shortcut.using
-
# @return [self]
-
1
def on_resolution(*args, &callback)
-
on_resolution_using @DefaultExecutor, *args, &callback
-
end
-
-
# Stores the callback to be executed synchronously on resolving thread after it is
-
# resolved.
-
#
-
# @!macro promises.param.args
-
# @!macro promise.param.callback
-
# @return [self]
-
#
-
# @overload an_event.on_resolution!(*args, &callback)
-
# @yield [*args] to the callback.
-
# @overload a_future.on_resolution!(*args, &callback)
-
# @yield [fulfilled, value, reason, *args] to the callback.
-
# @yieldparam [true, false] fulfilled
-
# @yieldparam [Object] value
-
# @yieldparam [Object] reason
-
1
def on_resolution!(*args, &callback)
-
add_callback :callback_on_resolution, args, callback
-
end
-
-
# Stores the callback to be executed asynchronously on executor after it is resolved.
-
#
-
# @!macro promises.param.executor
-
# @!macro promises.param.args
-
# @!macro promise.param.callback
-
# @return [self]
-
#
-
# @overload an_event.on_resolution_using(executor, *args, &callback)
-
# @yield [*args] to the callback.
-
# @overload a_future.on_resolution_using(executor, *args, &callback)
-
# @yield [fulfilled, value, reason, *args] to the callback.
-
# @yieldparam [true, false] fulfilled
-
# @yieldparam [Object] value
-
# @yieldparam [Object] reason
-
1
def on_resolution_using(executor, *args, &callback)
-
add_callback :async_callback_on_resolution, executor, args, callback
-
end
-
-
# @!macro promises.method.with_default_executor
-
# Crates new object with same class with the executor set as its new default executor.
-
# Any futures depending on it will use the new default executor.
-
# @!macro promises.shortcut.event-future
-
# @abstract
-
# @return [AbstractEventFuture]
-
1
def with_default_executor(executor)
-
raise NotImplementedError
-
end
-
-
# @!visibility private
-
1
def resolve_with(state, raise_on_reassign = true, reserved = false)
-
if compare_and_set_internal_state(reserved ? RESERVED : PENDING, state)
-
# go to synchronized block only if there were waiting threads
-
@Lock.synchronize { @Condition.broadcast } unless @Waiters.value == 0
-
call_callbacks state
-
else
-
return rejected_resolution(raise_on_reassign, state)
-
end
-
self
-
end
-
-
# For inspection.
-
# @!visibility private
-
# @return [Array<AbstractPromise>]
-
1
def blocks
-
@Callbacks.each_with_object([]) do |(method, args), promises|
-
promises.push(args[0]) if method == :callback_notify_blocked
-
end
-
end
-
-
# For inspection.
-
# @!visibility private
-
1
def callbacks
-
@Callbacks.each.to_a
-
end
-
-
# For inspection.
-
# @!visibility private
-
1
def promise
-
@Promise
-
end
-
-
# For inspection.
-
# @!visibility private
-
1
def touched?
-
promise.touched?
-
end
-
-
# For inspection.
-
# @!visibility private
-
1
def waiting_threads
-
@Waiters.each.to_a
-
end
-
-
# @!visibility private
-
1
def add_callback_notify_blocked(promise, index)
-
add_callback :callback_notify_blocked, promise, index
-
end
-
-
# @!visibility private
-
1
def add_callback_clear_delayed_node(node)
-
add_callback(:callback_clear_delayed_node, node)
-
end
-
-
# @!visibility private
-
1
def with_hidden_resolvable
-
# TODO (pitr-ch 10-Dec-2018): documentation, better name if in edge
-
self
-
end
-
-
1
private
-
-
1
def add_callback(method, *args)
-
state = internal_state
-
if state.resolved?
-
call_callback method, state, args
-
else
-
@Callbacks.push [method, args]
-
state = internal_state
-
# take back if it was resolved in the meanwhile
-
call_callbacks state if state.resolved?
-
end
-
self
-
end
-
-
1
def callback_clear_delayed_node(state, node)
-
node.value = nil
-
end
-
-
# @return [Boolean]
-
1
def wait_until_resolved(timeout)
-
return true if resolved?
-
-
touch
-
-
@Lock.synchronize do
-
@Waiters.increment
-
begin
-
unless resolved?
-
@Condition.wait @Lock, timeout
-
end
-
ensure
-
# JRuby may raise ConcurrencyError
-
@Waiters.decrement
-
end
-
end
-
resolved?
-
end
-
-
1
def call_callback(method, state, args)
-
self.send method, state, *args
-
end
-
-
1
def call_callbacks(state)
-
method, args = @Callbacks.pop
-
while method
-
call_callback method, state, args
-
method, args = @Callbacks.pop
-
end
-
end
-
-
1
def with_async(executor, *args, &block)
-
Concurrent.executor(executor).post(*args, &block)
-
end
-
-
1
def async_callback_on_resolution(state, executor, args, callback)
-
with_async(executor, state, args, callback) do |st, ar, cb|
-
callback_on_resolution st, ar, cb
-
end
-
end
-
-
1
def callback_notify_blocked(state, promise, index)
-
promise.on_blocker_resolution self, index
-
end
-
end
-
-
# Represents an event which will happen in future (will be resolved). The event is either
-
# pending or resolved. It should be always resolved. Use {Future} to communicate rejections and
-
# cancellation.
-
1
class Event < AbstractEventFuture
-
-
1
alias_method :then, :chain
-
-
-
# @!macro promises.method.zip
-
# Creates a new event or a future which will be resolved when receiver and other are.
-
# Returns an event if receiver and other are events, otherwise returns a future.
-
# If just one of the parties is Future then the result
-
# of the returned future is equal to the result of the supplied future. If both are futures
-
# then the result is as described in {FactoryMethods#zip_futures_on}.
-
#
-
# @return [Future, Event]
-
1
def zip(other)
-
if other.is_a?(Future)
-
ZipFutureEventPromise.new_blocked_by2(other, self, @DefaultExecutor).future
-
else
-
ZipEventEventPromise.new_blocked_by2(self, other, @DefaultExecutor).event
-
end
-
end
-
-
1
alias_method :&, :zip
-
-
# Creates a new event which will be resolved when the first of receiver, `event_or_future`
-
# resolves.
-
#
-
# @return [Event]
-
1
def any(event_or_future)
-
AnyResolvedEventPromise.new_blocked_by2(self, event_or_future, @DefaultExecutor).event
-
end
-
-
1
alias_method :|, :any
-
-
# Creates new event dependent on receiver which will not evaluate until touched, see {#touch}.
-
# In other words, it inserts delay into the chain of Futures making rest of it lazy evaluated.
-
#
-
# @return [Event]
-
1
def delay
-
event = DelayPromise.new(@DefaultExecutor).event
-
ZipEventEventPromise.new_blocked_by2(self, event, @DefaultExecutor).event
-
end
-
-
# @!macro promise.method.schedule
-
# Creates new event dependent on receiver scheduled to execute on/in intended_time.
-
# In time is interpreted from the moment the receiver is resolved, therefore it inserts
-
# delay into the chain.
-
#
-
# @!macro promises.param.intended_time
-
# @return [Event]
-
1
def schedule(intended_time)
-
chain do
-
event = ScheduledPromise.new(@DefaultExecutor, intended_time).event
-
ZipEventEventPromise.new_blocked_by2(self, event, @DefaultExecutor).event
-
end.flat_event
-
end
-
-
# Converts event to a future. The future is fulfilled when the event is resolved, the future may never fail.
-
#
-
# @return [Future]
-
1
def to_future
-
future = Promises.resolvable_future
-
ensure
-
chain_resolvable(future)
-
end
-
-
# Returns self, since this is event
-
# @return [Event]
-
1
def to_event
-
self
-
end
-
-
# @!macro promises.method.with_default_executor
-
# @return [Event]
-
1
def with_default_executor(executor)
-
EventWrapperPromise.new_blocked_by1(self, executor).event
-
end
-
-
1
private
-
-
1
def rejected_resolution(raise_on_reassign, state)
-
Concurrent::MultipleAssignmentError.new('Event can be resolved only once') if raise_on_reassign
-
return false
-
end
-
-
1
def callback_on_resolution(state, args, callback)
-
callback.call(*args)
-
end
-
end
-
-
# Represents a value which will become available in future. May reject with a reason instead,
-
# e.g. when the tasks raises an exception.
-
1
class Future < AbstractEventFuture
-
-
# Is it in fulfilled state?
-
# @return [Boolean]
-
1
def fulfilled?
-
state = internal_state
-
state.resolved? && state.fulfilled?
-
end
-
-
# Is it in rejected state?
-
# @return [Boolean]
-
1
def rejected?
-
state = internal_state
-
state.resolved? && !state.fulfilled?
-
end
-
-
# @!macro promises.warn.nil
-
# @note Make sure returned `nil` is not confused with timeout, no value when rejected,
-
# no reason when fulfilled, etc.
-
# Use more exact methods if needed, like {#wait}, {#value!}, {#result}, etc.
-
-
# @!macro promises.method.value
-
# Return value of the future.
-
# @!macro promises.touches
-
#
-
# @!macro promises.warn.blocks
-
# @!macro promises.warn.nil
-
# @!macro promises.param.timeout
-
# @!macro promises.param.timeout_value
-
# @param [Object] timeout_value a value returned by the method when it times out
-
# @return [Object, nil, timeout_value] the value of the Future when fulfilled,
-
# timeout_value on timeout,
-
# nil on rejection.
-
1
def value(timeout = nil, timeout_value = nil)
-
if wait_until_resolved timeout
-
internal_state.value
-
else
-
timeout_value
-
end
-
end
-
-
# Returns reason of future's rejection.
-
# @!macro promises.touches
-
#
-
# @!macro promises.warn.blocks
-
# @!macro promises.warn.nil
-
# @!macro promises.param.timeout
-
# @!macro promises.param.timeout_value
-
# @return [Object, timeout_value] the reason, or timeout_value on timeout, or nil on fulfillment.
-
1
def reason(timeout = nil, timeout_value = nil)
-
if wait_until_resolved timeout
-
internal_state.reason
-
else
-
timeout_value
-
end
-
end
-
-
# Returns triplet fulfilled?, value, reason.
-
# @!macro promises.touches
-
#
-
# @!macro promises.warn.blocks
-
# @!macro promises.param.timeout
-
# @return [Array(Boolean, Object, Object), nil] triplet of fulfilled?, value, reason, or nil
-
# on timeout.
-
1
def result(timeout = nil)
-
internal_state.result if wait_until_resolved timeout
-
end
-
-
# @!macro promises.method.wait
-
# @raise [Exception] {#reason} on rejection
-
1
def wait!(timeout = nil)
-
result = wait_until_resolved!(timeout)
-
timeout ? result : self
-
end
-
-
# @!macro promises.method.value
-
# @return [Object, nil, timeout_value] the value of the Future when fulfilled,
-
# or nil on rejection,
-
# or timeout_value on timeout.
-
# @raise [Exception] {#reason} on rejection
-
1
def value!(timeout = nil, timeout_value = nil)
-
if wait_until_resolved! timeout
-
internal_state.value
-
else
-
timeout_value
-
end
-
end
-
-
# Allows rejected Future to be risen with `raise` method.
-
# If the reason is not an exception `Runtime.new(reason)` is returned.
-
#
-
# @example
-
# raise Promises.rejected_future(StandardError.new("boom"))
-
# raise Promises.rejected_future("or just boom")
-
# @raise [Concurrent::Error] when raising not rejected future
-
# @return [Exception]
-
1
def exception(*args)
-
raise Concurrent::Error, 'it is not rejected' unless rejected?
-
raise ArgumentError unless args.size <= 1
-
reason = Array(internal_state.reason).flatten.compact
-
if reason.size > 1
-
ex = Concurrent::MultipleErrors.new reason
-
ex.set_backtrace(caller)
-
ex
-
else
-
ex = if reason[0].respond_to? :exception
-
reason[0].exception(*args)
-
else
-
RuntimeError.new(reason[0]).exception(*args)
-
end
-
ex.set_backtrace Array(ex.backtrace) + caller
-
ex
-
end
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future]
-
1
def then(*args, &task)
-
then_on @DefaultExecutor, *args, &task
-
end
-
-
# Chains the task to be executed asynchronously on executor after it fulfills. Does not run
-
# the task if it rejects. It will resolve though, triggering any dependent futures.
-
#
-
# @!macro promises.param.executor
-
# @!macro promises.param.args
-
# @!macro promise.param.task-future
-
# @return [Future]
-
# @yield [value, *args] to the task.
-
1
def then_on(executor, *args, &task)
-
ThenPromise.new_blocked_by1(self, @DefaultExecutor, executor, args, &task).future
-
end
-
-
# @!macro promises.shortcut.on
-
# @return [Future]
-
1
def rescue(*args, &task)
-
rescue_on @DefaultExecutor, *args, &task
-
end
-
-
# Chains the task to be executed asynchronously on executor after it rejects. Does not run
-
# the task if it fulfills. It will resolve though, triggering any dependent futures.
-
#
-
# @!macro promises.param.executor
-
# @!macro promises.param.args
-
# @!macro promise.param.task-future
-
# @return [Future]
-
# @yield [reason, *args] to the task.
-
1
def rescue_on(executor, *args, &task)
-
RescuePromise.new_blocked_by1(self, @DefaultExecutor, executor, args, &task).future
-
end
-
-
# @!macro promises.method.zip
-
# @return [Future]
-
1
def zip(other)
-
if other.is_a?(Future)
-
ZipFuturesPromise.new_blocked_by2(self, other, @DefaultExecutor).future
-
else
-
ZipFutureEventPromise.new_blocked_by2(self, other, @DefaultExecutor).future
-
end
-
end
-
-
1
alias_method :&, :zip
-
-
# Creates a new event which will be resolved when the first of receiver, `event_or_future`
-
# resolves. Returning future will have value nil if event_or_future is event and resolves
-
# first.
-
#
-
# @return [Future]
-
1
def any(event_or_future)
-
AnyResolvedFuturePromise.new_blocked_by2(self, event_or_future, @DefaultExecutor).future
-
end
-
-
1
alias_method :|, :any
-
-
# Creates new future dependent on receiver which will not evaluate until touched, see {#touch}.
-
# In other words, it inserts delay into the chain of Futures making rest of it lazy evaluated.
-
#
-
# @return [Future]
-
1
def delay
-
event = DelayPromise.new(@DefaultExecutor).event
-
ZipFutureEventPromise.new_blocked_by2(self, event, @DefaultExecutor).future
-
end
-
-
# @!macro promise.method.schedule
-
# @return [Future]
-
1
def schedule(intended_time)
-
chain do
-
event = ScheduledPromise.new(@DefaultExecutor, intended_time).event
-
ZipFutureEventPromise.new_blocked_by2(self, event, @DefaultExecutor).future
-
end.flat
-
end
-
-
# @!macro promises.method.with_default_executor
-
# @return [Future]
-
1
def with_default_executor(executor)
-
FutureWrapperPromise.new_blocked_by1(self, executor).future
-
end
-
-
# Creates new future which will have result of the future returned by receiver. If receiver
-
# rejects it will have its rejection.
-
#
-
# @param [Integer] level how many levels of futures should flatten
-
# @return [Future]
-
1
def flat_future(level = 1)
-
FlatFuturePromise.new_blocked_by1(self, level, @DefaultExecutor).future
-
end
-
-
1
alias_method :flat, :flat_future
-
-
# Creates new event which will be resolved when the returned event by receiver is.
-
# Be careful if the receiver rejects it will just resolve since Event does not hold reason.
-
#
-
# @return [Event]
-
1
def flat_event
-
FlatEventPromise.new_blocked_by1(self, @DefaultExecutor).event
-
end
-
-
# @!macro promises.shortcut.using
-
# @return [self]
-
1
def on_fulfillment(*args, &callback)
-
on_fulfillment_using @DefaultExecutor, *args, &callback
-
end
-
-
# Stores the callback to be executed synchronously on resolving thread after it is
-
# fulfilled. Does nothing on rejection.
-
#
-
# @!macro promises.param.args
-
# @!macro promise.param.callback
-
# @return [self]
-
# @yield [value, *args] to the callback.
-
1
def on_fulfillment!(*args, &callback)
-
add_callback :callback_on_fulfillment, args, callback
-
end
-
-
# Stores the callback to be executed asynchronously on executor after it is
-
# fulfilled. Does nothing on rejection.
-
#
-
# @!macro promises.param.executor
-
# @!macro promises.param.args
-
# @!macro promise.param.callback
-
# @return [self]
-
# @yield [value, *args] to the callback.
-
1
def on_fulfillment_using(executor, *args, &callback)
-
add_callback :async_callback_on_fulfillment, executor, args, callback
-
end
-
-
# @!macro promises.shortcut.using
-
# @return [self]
-
1
def on_rejection(*args, &callback)
-
on_rejection_using @DefaultExecutor, *args, &callback
-
end
-
-
# Stores the callback to be executed synchronously on resolving thread after it is
-
# rejected. Does nothing on fulfillment.
-
#
-
# @!macro promises.param.args
-
# @!macro promise.param.callback
-
# @return [self]
-
# @yield [reason, *args] to the callback.
-
1
def on_rejection!(*args, &callback)
-
add_callback :callback_on_rejection, args, callback
-
end
-
-
# Stores the callback to be executed asynchronously on executor after it is
-
# rejected. Does nothing on fulfillment.
-
#
-
# @!macro promises.param.executor
-
# @!macro promises.param.args
-
# @!macro promise.param.callback
-
# @return [self]
-
# @yield [reason, *args] to the callback.
-
1
def on_rejection_using(executor, *args, &callback)
-
add_callback :async_callback_on_rejection, executor, args, callback
-
end
-
-
# Allows to use futures as green threads. The receiver has to evaluate to a future which
-
# represents what should be done next. It basically flattens indefinitely until non Future
-
# values is returned which becomes result of the returned future. Any encountered exception
-
# will become reason of the returned future.
-
#
-
# @return [Future]
-
# @param [#call(value)] run_test
-
# an object which when called returns either Future to keep running with
-
# or nil, then the run completes with the value.
-
# The run_test can be used to extract the Future from deeper structure,
-
# or to distinguish Future which is a resulting value from a future
-
# which is suppose to continue running.
-
# @example
-
# body = lambda do |v|
-
# v += 1
-
# v < 5 ? Promises.future(v, &body) : v
-
# end
-
# Promises.future(0, &body).run.value! # => 5
-
1
def run(run_test = method(:run_test))
-
RunFuturePromise.new_blocked_by1(self, @DefaultExecutor, run_test).future
-
end
-
-
# @!visibility private
-
1
def apply(args, block)
-
internal_state.apply args, block
-
end
-
-
# Converts future to event which is resolved when future is resolved by fulfillment or rejection.
-
#
-
# @return [Event]
-
1
def to_event
-
event = Promises.resolvable_event
-
ensure
-
chain_resolvable(event)
-
end
-
-
# Returns self, since this is a future
-
# @return [Future]
-
1
def to_future
-
self
-
end
-
-
# @return [String] Short string representation.
-
1
def to_s
-
if resolved?
-
format '%s with %s>', super[0..-2], (fulfilled? ? value : reason).inspect
-
else
-
super
-
end
-
end
-
-
1
alias_method :inspect, :to_s
-
-
1
private
-
-
1
def run_test(v)
-
v if v.is_a?(Future)
-
end
-
-
1
def rejected_resolution(raise_on_reassign, state)
-
if raise_on_reassign
-
if internal_state == RESERVED
-
raise Concurrent::MultipleAssignmentError.new(
-
"Future can be resolved only once. It is already reserved.")
-
else
-
raise Concurrent::MultipleAssignmentError.new(
-
"Future can be resolved only once. It's #{result}, trying to set #{state.result}.",
-
current_result: result,
-
new_result: state.result)
-
end
-
end
-
return false
-
end
-
-
1
def wait_until_resolved!(timeout = nil)
-
result = wait_until_resolved(timeout)
-
raise self if rejected?
-
result
-
end
-
-
1
def async_callback_on_fulfillment(state, executor, args, callback)
-
with_async(executor, state, args, callback) do |st, ar, cb|
-
callback_on_fulfillment st, ar, cb
-
end
-
end
-
-
1
def async_callback_on_rejection(state, executor, args, callback)
-
with_async(executor, state, args, callback) do |st, ar, cb|
-
callback_on_rejection st, ar, cb
-
end
-
end
-
-
1
def callback_on_fulfillment(state, args, callback)
-
state.apply args, callback if state.fulfilled?
-
end
-
-
1
def callback_on_rejection(state, args, callback)
-
state.apply args, callback unless state.fulfilled?
-
end
-
-
1
def callback_on_resolution(state, args, callback)
-
callback.call(*state.result, *args)
-
end
-
-
end
-
-
# Marker module of Future, Event resolved manually.
-
1
module Resolvable
-
1
include InternalStates
-
end
-
-
# A Event which can be resolved by user.
-
1
class ResolvableEvent < Event
-
1
include Resolvable
-
-
# @!macro raise_on_reassign
-
# @raise [MultipleAssignmentError] when already resolved and raise_on_reassign is true.
-
-
# @!macro promise.param.raise_on_reassign
-
# @param [Boolean] raise_on_reassign should method raise exception if already resolved
-
# @return [self, false] false is returner when raise_on_reassign is false and the receiver
-
# is already resolved.
-
#
-
-
# Makes the event resolved, which triggers all dependent futures.
-
#
-
# @!macro promise.param.raise_on_reassign
-
# @!macro promise.param.reserved
-
# @param [true, false] reserved
-
# Set to true if the resolvable is {#reserve}d by you,
-
# marks resolution of reserved resolvable events and futures explicitly.
-
# Advanced feature, ignore unless you use {Resolvable#reserve} from edge.
-
1
def resolve(raise_on_reassign = true, reserved = false)
-
resolve_with RESOLVED, raise_on_reassign, reserved
-
end
-
-
# Creates new event wrapping receiver, effectively hiding the resolve method.
-
#
-
# @return [Event]
-
1
def with_hidden_resolvable
-
@with_hidden_resolvable ||= EventWrapperPromise.new_blocked_by1(self, @DefaultExecutor).event
-
end
-
-
# Behaves as {AbstractEventFuture#wait} but has one additional optional argument
-
# resolve_on_timeout.
-
#
-
# @param [true, false] resolve_on_timeout
-
# If it times out and the argument is true it will also resolve the event.
-
# @return [self, true, false]
-
# @see AbstractEventFuture#wait
-
1
def wait(timeout = nil, resolve_on_timeout = false)
-
super(timeout) or if resolve_on_timeout
-
# if it fails to resolve it was resolved in the meantime
-
# so return true as if there was no timeout
-
!resolve(false)
-
else
-
false
-
end
-
end
-
end
-
-
# A Future which can be resolved by user.
-
1
class ResolvableFuture < Future
-
1
include Resolvable
-
-
# Makes the future resolved with result of triplet `fulfilled?`, `value`, `reason`,
-
# which triggers all dependent futures.
-
#
-
# @param [true, false] fulfilled
-
# @param [Object] value
-
# @param [Object] reason
-
# @!macro promise.param.raise_on_reassign
-
# @!macro promise.param.reserved
-
1
def resolve(fulfilled = true, value = nil, reason = nil, raise_on_reassign = true, reserved = false)
-
resolve_with(fulfilled ? Fulfilled.new(value) : Rejected.new(reason), raise_on_reassign, reserved)
-
end
-
-
# Makes the future fulfilled with `value`,
-
# which triggers all dependent futures.
-
#
-
# @param [Object] value
-
# @!macro promise.param.raise_on_reassign
-
# @!macro promise.param.reserved
-
1
def fulfill(value, raise_on_reassign = true, reserved = false)
-
resolve_with Fulfilled.new(value), raise_on_reassign, reserved
-
end
-
-
# Makes the future rejected with `reason`,
-
# which triggers all dependent futures.
-
#
-
# @param [Object] reason
-
# @!macro promise.param.raise_on_reassign
-
# @!macro promise.param.reserved
-
1
def reject(reason, raise_on_reassign = true, reserved = false)
-
resolve_with Rejected.new(reason), raise_on_reassign, reserved
-
end
-
-
# Evaluates the block and sets its result as future's value fulfilling, if the block raises
-
# an exception the future rejects with it.
-
#
-
# @yield [*args] to the block.
-
# @yieldreturn [Object] value
-
# @return [self]
-
1
def evaluate_to(*args, &block)
-
promise.evaluate_to(*args, block)
-
end
-
-
# Evaluates the block and sets its result as future's value fulfilling, if the block raises
-
# an exception the future rejects with it.
-
#
-
# @yield [*args] to the block.
-
# @yieldreturn [Object] value
-
# @return [self]
-
# @raise [Exception] also raise reason on rejection.
-
1
def evaluate_to!(*args, &block)
-
promise.evaluate_to(*args, block).wait!
-
end
-
-
# @!macro promises.resolvable.resolve_on_timeout
-
# @param [::Array(true, Object, nil), ::Array(false, nil, Exception), nil] resolve_on_timeout
-
# If it times out and the argument is not nil it will also resolve the future
-
# to the provided resolution.
-
-
# Behaves as {AbstractEventFuture#wait} but has one additional optional argument
-
# resolve_on_timeout.
-
#
-
# @!macro promises.resolvable.resolve_on_timeout
-
# @return [self, true, false]
-
# @see AbstractEventFuture#wait
-
1
def wait(timeout = nil, resolve_on_timeout = nil)
-
super(timeout) or if resolve_on_timeout
-
# if it fails to resolve it was resolved in the meantime
-
# so return true as if there was no timeout
-
!resolve(*resolve_on_timeout, false)
-
else
-
false
-
end
-
end
-
-
# Behaves as {Future#wait!} but has one additional optional argument
-
# resolve_on_timeout.
-
#
-
# @!macro promises.resolvable.resolve_on_timeout
-
# @return [self, true, false]
-
# @raise [Exception] {#reason} on rejection
-
# @see Future#wait!
-
1
def wait!(timeout = nil, resolve_on_timeout = nil)
-
super(timeout) or if resolve_on_timeout
-
if resolve(*resolve_on_timeout, false)
-
false
-
else
-
# if it fails to resolve it was resolved in the meantime
-
# so return true as if there was no timeout
-
raise self if rejected?
-
true
-
end
-
else
-
false
-
end
-
end
-
-
# Behaves as {Future#value} but has one additional optional argument
-
# resolve_on_timeout.
-
#
-
# @!macro promises.resolvable.resolve_on_timeout
-
# @return [Object, timeout_value, nil]
-
# @see Future#value
-
1
def value(timeout = nil, timeout_value = nil, resolve_on_timeout = nil)
-
if wait_until_resolved timeout
-
internal_state.value
-
else
-
if resolve_on_timeout
-
unless resolve(*resolve_on_timeout, false)
-
# if it fails to resolve it was resolved in the meantime
-
# so return value as if there was no timeout
-
return internal_state.value
-
end
-
end
-
timeout_value
-
end
-
end
-
-
# Behaves as {Future#value!} but has one additional optional argument
-
# resolve_on_timeout.
-
#
-
# @!macro promises.resolvable.resolve_on_timeout
-
# @return [Object, timeout_value, nil]
-
# @raise [Exception] {#reason} on rejection
-
# @see Future#value!
-
1
def value!(timeout = nil, timeout_value = nil, resolve_on_timeout = nil)
-
if wait_until_resolved! timeout
-
internal_state.value
-
else
-
if resolve_on_timeout
-
unless resolve(*resolve_on_timeout, false)
-
# if it fails to resolve it was resolved in the meantime
-
# so return value as if there was no timeout
-
raise self if rejected?
-
return internal_state.value
-
end
-
end
-
timeout_value
-
end
-
end
-
-
# Behaves as {Future#reason} but has one additional optional argument
-
# resolve_on_timeout.
-
#
-
# @!macro promises.resolvable.resolve_on_timeout
-
# @return [Exception, timeout_value, nil]
-
# @see Future#reason
-
1
def reason(timeout = nil, timeout_value = nil, resolve_on_timeout = nil)
-
if wait_until_resolved timeout
-
internal_state.reason
-
else
-
if resolve_on_timeout
-
unless resolve(*resolve_on_timeout, false)
-
# if it fails to resolve it was resolved in the meantime
-
# so return value as if there was no timeout
-
return internal_state.reason
-
end
-
end
-
timeout_value
-
end
-
end
-
-
# Behaves as {Future#result} but has one additional optional argument
-
# resolve_on_timeout.
-
#
-
# @!macro promises.resolvable.resolve_on_timeout
-
# @return [::Array(Boolean, Object, Exception), nil]
-
# @see Future#result
-
1
def result(timeout = nil, resolve_on_timeout = nil)
-
if wait_until_resolved timeout
-
internal_state.result
-
else
-
if resolve_on_timeout
-
unless resolve(*resolve_on_timeout, false)
-
# if it fails to resolve it was resolved in the meantime
-
# so return value as if there was no timeout
-
internal_state.result
-
end
-
end
-
# otherwise returns nil
-
end
-
end
-
-
# Creates new future wrapping receiver, effectively hiding the resolve method and similar.
-
#
-
# @return [Future]
-
1
def with_hidden_resolvable
-
@with_hidden_resolvable ||= FutureWrapperPromise.new_blocked_by1(self, @DefaultExecutor).future
-
end
-
end
-
-
# @abstract
-
# @private
-
1
class AbstractPromise < Synchronization::Object
-
1
safe_initialization!
-
1
include InternalStates
-
-
1
def initialize(future)
-
super()
-
@Future = future
-
end
-
-
1
def future
-
@Future
-
end
-
-
1
alias_method :event, :future
-
-
1
def default_executor
-
future.default_executor
-
end
-
-
1
def state
-
future.state
-
end
-
-
1
def touch
-
end
-
-
1
def to_s
-
format '%s %s>', super[0..-2], @Future
-
end
-
-
1
alias_method :inspect, :to_s
-
-
1
def delayed_because
-
nil
-
end
-
-
1
private
-
-
1
def resolve_with(new_state, raise_on_reassign = true)
-
@Future.resolve_with(new_state, raise_on_reassign)
-
end
-
-
# @return [Future]
-
1
def evaluate_to(*args, block)
-
resolve_with Fulfilled.new(block.call(*args))
-
rescue Exception => error
-
resolve_with Rejected.new(error)
-
raise error unless error.is_a?(StandardError)
-
end
-
end
-
-
1
class ResolvableEventPromise < AbstractPromise
-
1
def initialize(default_executor)
-
super ResolvableEvent.new(self, default_executor)
-
end
-
end
-
-
1
class ResolvableFuturePromise < AbstractPromise
-
1
def initialize(default_executor)
-
super ResolvableFuture.new(self, default_executor)
-
end
-
-
1
public :evaluate_to
-
end
-
-
# @abstract
-
1
class InnerPromise < AbstractPromise
-
end
-
-
# @abstract
-
1
class BlockedPromise < InnerPromise
-
-
1
private_class_method :new
-
-
1
def self.new_blocked_by1(blocker, *args, &block)
-
blocker_delayed = blocker.promise.delayed_because
-
promise = new(blocker_delayed, 1, *args, &block)
-
blocker.add_callback_notify_blocked promise, 0
-
promise
-
end
-
-
1
def self.new_blocked_by2(blocker1, blocker2, *args, &block)
-
blocker_delayed1 = blocker1.promise.delayed_because
-
blocker_delayed2 = blocker2.promise.delayed_because
-
delayed = if blocker_delayed1 && blocker_delayed2
-
# TODO (pitr-ch 23-Dec-2016): use arrays when we know it will not grow (only flat adds delay)
-
LockFreeStack.of2(blocker_delayed1, blocker_delayed2)
-
else
-
blocker_delayed1 || blocker_delayed2
-
end
-
promise = new(delayed, 2, *args, &block)
-
blocker1.add_callback_notify_blocked promise, 0
-
blocker2.add_callback_notify_blocked promise, 1
-
promise
-
end
-
-
1
def self.new_blocked_by(blockers, *args, &block)
-
delayed = blockers.reduce(nil) { |d, f| add_delayed d, f.promise.delayed_because }
-
promise = new(delayed, blockers.size, *args, &block)
-
blockers.each_with_index { |f, i| f.add_callback_notify_blocked promise, i }
-
promise
-
end
-
-
1
def self.add_delayed(delayed1, delayed2)
-
if delayed1 && delayed2
-
delayed1.push delayed2
-
delayed1
-
else
-
delayed1 || delayed2
-
end
-
end
-
-
1
def initialize(delayed, blockers_count, future)
-
super(future)
-
@Delayed = delayed
-
@Countdown = AtomicFixnum.new blockers_count
-
end
-
-
1
def on_blocker_resolution(future, index)
-
countdown = process_on_blocker_resolution(future, index)
-
resolvable = resolvable?(countdown, future, index)
-
-
on_resolvable(future, index) if resolvable
-
end
-
-
1
def delayed_because
-
@Delayed
-
end
-
-
1
def touch
-
clear_and_propagate_touch
-
end
-
-
# for inspection only
-
1
def blocked_by
-
blocked_by = []
-
ObjectSpace.each_object(AbstractEventFuture) { |o| blocked_by.push o if o.blocks.include? self }
-
blocked_by
-
end
-
-
1
private
-
-
1
def clear_and_propagate_touch(stack_or_element = @Delayed)
-
return if stack_or_element.nil?
-
-
if stack_or_element.is_a? LockFreeStack
-
stack_or_element.clear_each { |element| clear_and_propagate_touch element }
-
else
-
stack_or_element.touch unless stack_or_element.nil? # if still present
-
end
-
end
-
-
# @return [true,false] if resolvable
-
1
def resolvable?(countdown, future, index)
-
countdown.zero?
-
end
-
-
1
def process_on_blocker_resolution(future, index)
-
@Countdown.decrement
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
raise NotImplementedError
-
end
-
end
-
-
# @abstract
-
1
class BlockedTaskPromise < BlockedPromise
-
1
def initialize(delayed, blockers_count, default_executor, executor, args, &task)
-
raise ArgumentError, 'no block given' unless block_given?
-
super delayed, 1, Future.new(self, default_executor)
-
@Executor = executor
-
@Task = task
-
@Args = args
-
end
-
-
1
def executor
-
@Executor
-
end
-
end
-
-
1
class ThenPromise < BlockedTaskPromise
-
1
private
-
-
1
def initialize(delayed, blockers_count, default_executor, executor, args, &task)
-
super delayed, blockers_count, default_executor, executor, args, &task
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
if resolved_future.fulfilled?
-
Concurrent.executor(@Executor).post(resolved_future, @Args, @Task) do |future, args, task|
-
evaluate_to lambda { future.apply args, task }
-
end
-
else
-
resolve_with resolved_future.internal_state
-
end
-
end
-
end
-
-
1
class RescuePromise < BlockedTaskPromise
-
1
private
-
-
1
def initialize(delayed, blockers_count, default_executor, executor, args, &task)
-
super delayed, blockers_count, default_executor, executor, args, &task
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
if resolved_future.rejected?
-
Concurrent.executor(@Executor).post(resolved_future, @Args, @Task) do |future, args, task|
-
evaluate_to lambda { future.apply args, task }
-
end
-
else
-
resolve_with resolved_future.internal_state
-
end
-
end
-
end
-
-
1
class ChainPromise < BlockedTaskPromise
-
1
private
-
-
1
def on_resolvable(resolved_future, index)
-
if Future === resolved_future
-
Concurrent.executor(@Executor).post(resolved_future, @Args, @Task) do |future, args, task|
-
evaluate_to(*future.result, *args, task)
-
end
-
else
-
Concurrent.executor(@Executor).post(@Args, @Task) do |args, task|
-
evaluate_to(*args, task)
-
end
-
end
-
end
-
end
-
-
# will be immediately resolved
-
1
class ImmediateEventPromise < InnerPromise
-
1
def initialize(default_executor)
-
super Event.new(self, default_executor).resolve_with(RESOLVED)
-
end
-
end
-
-
1
class ImmediateFuturePromise < InnerPromise
-
1
def initialize(default_executor, fulfilled, value, reason)
-
super Future.new(self, default_executor).
-
resolve_with(fulfilled ? Fulfilled.new(value) : Rejected.new(reason))
-
end
-
end
-
-
1
class AbstractFlatPromise < BlockedPromise
-
-
1
def initialize(delayed_because, blockers_count, event_or_future)
-
delayed = LockFreeStack.of1(self)
-
super(delayed, blockers_count, event_or_future)
-
# noinspection RubyArgCount
-
@Touched = AtomicBoolean.new false
-
@DelayedBecause = delayed_because || LockFreeStack.new
-
-
event_or_future.add_callback_clear_delayed_node delayed.peek
-
end
-
-
1
def touch
-
if @Touched.make_true
-
clear_and_propagate_touch @DelayedBecause
-
end
-
end
-
-
1
private
-
-
1
def touched?
-
@Touched.value
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
resolve_with resolved_future.internal_state
-
end
-
-
1
def resolvable?(countdown, future, index)
-
!@Future.internal_state.resolved? && super(countdown, future, index)
-
end
-
-
1
def add_delayed_of(future)
-
delayed = future.promise.delayed_because
-
if touched?
-
clear_and_propagate_touch delayed
-
else
-
BlockedPromise.add_delayed @DelayedBecause, delayed
-
clear_and_propagate_touch @DelayedBecause if touched?
-
end
-
end
-
-
end
-
-
1
class FlatEventPromise < AbstractFlatPromise
-
-
1
private
-
-
1
def initialize(delayed, blockers_count, default_executor)
-
super delayed, 2, Event.new(self, default_executor)
-
end
-
-
1
def process_on_blocker_resolution(future, index)
-
countdown = super(future, index)
-
if countdown.nonzero?
-
internal_state = future.internal_state
-
-
unless internal_state.fulfilled?
-
resolve_with RESOLVED
-
return countdown
-
end
-
-
value = internal_state.value
-
case value
-
when AbstractEventFuture
-
add_delayed_of value
-
value.add_callback_notify_blocked self, nil
-
countdown
-
else
-
resolve_with RESOLVED
-
end
-
end
-
countdown
-
end
-
-
end
-
-
1
class FlatFuturePromise < AbstractFlatPromise
-
-
1
private
-
-
1
def initialize(delayed, blockers_count, levels, default_executor)
-
raise ArgumentError, 'levels has to be higher than 0' if levels < 1
-
# flat promise may result to a future having delayed futures, therefore we have to have empty stack
-
# to be able to add new delayed futures
-
super delayed || LockFreeStack.new, 1 + levels, Future.new(self, default_executor)
-
end
-
-
1
def process_on_blocker_resolution(future, index)
-
countdown = super(future, index)
-
if countdown.nonzero?
-
internal_state = future.internal_state
-
-
unless internal_state.fulfilled?
-
resolve_with internal_state
-
return countdown
-
end
-
-
value = internal_state.value
-
case value
-
when AbstractEventFuture
-
add_delayed_of value
-
value.add_callback_notify_blocked self, nil
-
countdown
-
else
-
evaluate_to(lambda { raise TypeError, "returned value #{value.inspect} is not a Future" })
-
end
-
end
-
countdown
-
end
-
-
end
-
-
1
class RunFuturePromise < AbstractFlatPromise
-
-
1
private
-
-
1
def initialize(delayed, blockers_count, default_executor, run_test)
-
super delayed, 1, Future.new(self, default_executor)
-
@RunTest = run_test
-
end
-
-
1
def process_on_blocker_resolution(future, index)
-
internal_state = future.internal_state
-
-
unless internal_state.fulfilled?
-
resolve_with internal_state
-
return 0
-
end
-
-
value = internal_state.value
-
continuation_future = @RunTest.call value
-
-
if continuation_future
-
add_delayed_of continuation_future
-
continuation_future.add_callback_notify_blocked self, nil
-
else
-
resolve_with internal_state
-
end
-
-
1
-
end
-
end
-
-
1
class ZipEventEventPromise < BlockedPromise
-
1
def initialize(delayed, blockers_count, default_executor)
-
super delayed, 2, Event.new(self, default_executor)
-
end
-
-
1
private
-
-
1
def on_resolvable(resolved_future, index)
-
resolve_with RESOLVED
-
end
-
end
-
-
1
class ZipFutureEventPromise < BlockedPromise
-
1
def initialize(delayed, blockers_count, default_executor)
-
super delayed, 2, Future.new(self, default_executor)
-
@result = nil
-
end
-
-
1
private
-
-
1
def process_on_blocker_resolution(future, index)
-
# first blocking is future, take its result
-
@result = future.internal_state if index == 0
-
# super has to be called after above to piggyback on volatile @Countdown
-
super future, index
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
resolve_with @result
-
end
-
end
-
-
1
class EventWrapperPromise < BlockedPromise
-
1
def initialize(delayed, blockers_count, default_executor)
-
super delayed, 1, Event.new(self, default_executor)
-
end
-
-
1
private
-
-
1
def on_resolvable(resolved_future, index)
-
resolve_with RESOLVED
-
end
-
end
-
-
1
class FutureWrapperPromise < BlockedPromise
-
1
def initialize(delayed, blockers_count, default_executor)
-
super delayed, 1, Future.new(self, default_executor)
-
end
-
-
1
private
-
-
1
def on_resolvable(resolved_future, index)
-
resolve_with resolved_future.internal_state
-
end
-
end
-
-
1
class ZipFuturesPromise < BlockedPromise
-
-
1
private
-
-
1
def initialize(delayed, blockers_count, default_executor)
-
super(delayed, blockers_count, Future.new(self, default_executor))
-
@Resolutions = ::Array.new(blockers_count, nil)
-
-
on_resolvable nil, nil if blockers_count == 0
-
end
-
-
1
def process_on_blocker_resolution(future, index)
-
# TODO (pitr-ch 18-Dec-2016): Can we assume that array will never break under parallel access when never re-sized?
-
@Resolutions[index] = future.internal_state # has to be set before countdown in super
-
super future, index
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
all_fulfilled = true
-
values = ::Array.new(@Resolutions.size)
-
reasons = ::Array.new(@Resolutions.size)
-
-
@Resolutions.each_with_index do |internal_state, i|
-
fulfilled, values[i], reasons[i] = internal_state.result
-
all_fulfilled &&= fulfilled
-
end
-
-
if all_fulfilled
-
resolve_with FulfilledArray.new(values)
-
else
-
resolve_with PartiallyRejected.new(values, reasons)
-
end
-
end
-
end
-
-
1
class ZipEventsPromise < BlockedPromise
-
-
1
private
-
-
1
def initialize(delayed, blockers_count, default_executor)
-
super delayed, blockers_count, Event.new(self, default_executor)
-
-
on_resolvable nil, nil if blockers_count == 0
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
resolve_with RESOLVED
-
end
-
end
-
-
# @abstract
-
1
class AbstractAnyPromise < BlockedPromise
-
end
-
-
1
class AnyResolvedEventPromise < AbstractAnyPromise
-
-
1
private
-
-
1
def initialize(delayed, blockers_count, default_executor)
-
super delayed, blockers_count, Event.new(self, default_executor)
-
end
-
-
1
def resolvable?(countdown, future, index)
-
true
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
resolve_with RESOLVED, false
-
end
-
end
-
-
1
class AnyResolvedFuturePromise < AbstractAnyPromise
-
-
1
private
-
-
1
def initialize(delayed, blockers_count, default_executor)
-
super delayed, blockers_count, Future.new(self, default_executor)
-
end
-
-
1
def resolvable?(countdown, future, index)
-
true
-
end
-
-
1
def on_resolvable(resolved_future, index)
-
resolve_with resolved_future.internal_state, false
-
end
-
end
-
-
1
class AnyFulfilledFuturePromise < AnyResolvedFuturePromise
-
-
1
private
-
-
1
def resolvable?(countdown, future, index)
-
future.fulfilled? ||
-
# inlined super from BlockedPromise
-
countdown.zero?
-
end
-
end
-
-
1
class DelayPromise < InnerPromise
-
-
1
def initialize(default_executor)
-
event = Event.new(self, default_executor)
-
@Delayed = LockFreeStack.of1(self)
-
super event
-
event.add_callback_clear_delayed_node @Delayed.peek
-
end
-
-
1
def touch
-
@Future.resolve_with RESOLVED
-
end
-
-
1
def delayed_because
-
@Delayed
-
end
-
-
end
-
-
1
class ScheduledPromise < InnerPromise
-
1
def intended_time
-
@IntendedTime
-
end
-
-
1
def inspect
-
"#{to_s[0..-2]} intended_time: #{@IntendedTime}>"
-
end
-
-
1
private
-
-
1
def initialize(default_executor, intended_time)
-
super Event.new(self, default_executor)
-
-
@IntendedTime = intended_time
-
-
in_seconds = begin
-
now = Time.now
-
schedule_time = if @IntendedTime.is_a? Time
-
@IntendedTime
-
else
-
now + @IntendedTime
-
end
-
[0, schedule_time.to_f - now.to_f].max
-
end
-
-
Concurrent.global_timer_set.post(in_seconds) do
-
@Future.resolve_with RESOLVED
-
end
-
end
-
end
-
-
1
extend FactoryMethods
-
-
1
private_constant :AbstractPromise,
-
:ResolvableEventPromise,
-
:ResolvableFuturePromise,
-
:InnerPromise,
-
:BlockedPromise,
-
:BlockedTaskPromise,
-
:ThenPromise,
-
:RescuePromise,
-
:ChainPromise,
-
:ImmediateEventPromise,
-
:ImmediateFuturePromise,
-
:AbstractFlatPromise,
-
:FlatFuturePromise,
-
:FlatEventPromise,
-
:RunFuturePromise,
-
:ZipEventEventPromise,
-
:ZipFutureEventPromise,
-
:EventWrapperPromise,
-
:FutureWrapperPromise,
-
:ZipFuturesPromise,
-
:ZipEventsPromise,
-
:AbstractAnyPromise,
-
:AnyResolvedFuturePromise,
-
:AnyFulfilledFuturePromise,
-
:AnyResolvedEventPromise,
-
:DelayPromise,
-
:ScheduledPromise
-
-
-
end
-
end
-
1
module Concurrent
-
-
# Methods form module A included to a module B, which is already included into class C,
-
# will not be visible in the C class. If this module is extended to B then A's methods
-
# are correctly made visible to C.
-
#
-
# @example
-
# module A
-
# def a
-
# :a
-
# end
-
# end
-
#
-
# module B1
-
# end
-
#
-
# class C1
-
# include B1
-
# end
-
#
-
# module B2
-
# extend Concurrent::ReInclude
-
# end
-
#
-
# class C2
-
# include B2
-
# end
-
#
-
# B1.send :include, A
-
# B2.send :include, A
-
#
-
# C1.new.respond_to? :a # => false
-
# C2.new.respond_to? :a # => true
-
1
module ReInclude
-
# @!visibility private
-
1
def included(base)
-
(@re_include_to_bases ||= []) << [:include, base]
-
super(base)
-
end
-
-
# @!visibility private
-
1
def extended(base)
-
2
(@re_include_to_bases ||= []) << [:extend, base]
-
2
super(base)
-
end
-
-
# @!visibility private
-
1
def include(*modules)
-
1
result = super(*modules)
-
1
modules.reverse.each do |module_being_included|
-
1
(@re_include_to_bases ||= []).each do |method, mod|
-
1
mod.send method, module_being_included
-
end
-
end
-
1
result
-
end
-
end
-
end
-
1
require 'concurrent/constants'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/configuration'
-
1
require 'concurrent/ivar'
-
1
require 'concurrent/collection/copy_on_notify_observer_set'
-
1
require 'concurrent/utility/monotonic_time'
-
-
1
require 'concurrent/options'
-
-
1
module Concurrent
-
-
# `ScheduledTask` is a close relative of `Concurrent::Future` but with one
-
# important difference: A `Future` is set to execute as soon as possible
-
# whereas a `ScheduledTask` is set to execute after a specified delay. This
-
# implementation is loosely based on Java's
-
# [ScheduledExecutorService](http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ScheduledExecutorService.html).
-
# It is a more feature-rich variant of {Concurrent.timer}.
-
#
-
# The *intended* schedule time of task execution is set on object construction
-
# with the `delay` argument. The delay is a numeric (floating point or integer)
-
# representing a number of seconds in the future. Any other value or a numeric
-
# equal to or less than zero will result in an exception. The *actual* schedule
-
# time of task execution is set when the `execute` method is called.
-
#
-
# The constructor can also be given zero or more processing options. Currently
-
# the only supported options are those recognized by the
-
# [Dereferenceable](Dereferenceable) module.
-
#
-
# The final constructor argument is a block representing the task to be performed.
-
# If no block is given an `ArgumentError` will be raised.
-
#
-
# **States**
-
#
-
# `ScheduledTask` mixes in the [Obligation](Obligation) module thus giving it
-
# "future" behavior. This includes the expected lifecycle states. `ScheduledTask`
-
# has one additional state, however. While the task (block) is being executed the
-
# state of the object will be `:processing`. This additional state is necessary
-
# because it has implications for task cancellation.
-
#
-
# **Cancellation**
-
#
-
# A `:pending` task can be cancelled using the `#cancel` method. A task in any
-
# other state, including `:processing`, cannot be cancelled. The `#cancel`
-
# method returns a boolean indicating the success of the cancellation attempt.
-
# A cancelled `ScheduledTask` cannot be restarted. It is immutable.
-
#
-
# **Obligation and Observation**
-
#
-
# The result of a `ScheduledTask` can be obtained either synchronously or
-
# asynchronously. `ScheduledTask` mixes in both the [Obligation](Obligation)
-
# module and the
-
# [Observable](http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html)
-
# module from the Ruby standard library. With one exception `ScheduledTask`
-
# behaves identically to [Future](Observable) with regard to these modules.
-
#
-
# @!macro copy_options
-
#
-
# @example Basic usage
-
#
-
# require 'concurrent'
-
# require 'thread' # for Queue
-
# require 'open-uri' # for open(uri)
-
#
-
# class Ticker
-
# def get_year_end_closing(symbol, year)
-
# uri = "http://ichart.finance.yahoo.com/table.csv?s=#{symbol}&a=11&b=01&c=#{year}&d=11&e=31&f=#{year}&g=m"
-
# data = open(uri) {|f| f.collect{|line| line.strip } }
-
# data[1].split(',')[4].to_f
-
# end
-
# end
-
#
-
# # Future
-
# price = Concurrent::Future.execute{ Ticker.new.get_year_end_closing('TWTR', 2013) }
-
# price.state #=> :pending
-
# sleep(1) # do other stuff
-
# price.value #=> 63.65
-
# price.state #=> :fulfilled
-
#
-
# # ScheduledTask
-
# task = Concurrent::ScheduledTask.execute(2){ Ticker.new.get_year_end_closing('INTC', 2013) }
-
# task.state #=> :pending
-
# sleep(3) # do other stuff
-
# task.value #=> 25.96
-
#
-
# @example Successful task execution
-
#
-
# task = Concurrent::ScheduledTask.new(2){ 'What does the fox say?' }
-
# task.state #=> :unscheduled
-
# task.execute
-
# task.state #=> pending
-
#
-
# # wait for it...
-
# sleep(3)
-
#
-
# task.unscheduled? #=> false
-
# task.pending? #=> false
-
# task.fulfilled? #=> true
-
# task.rejected? #=> false
-
# task.value #=> 'What does the fox say?'
-
#
-
# @example One line creation and execution
-
#
-
# task = Concurrent::ScheduledTask.new(2){ 'What does the fox say?' }.execute
-
# task.state #=> pending
-
#
-
# task = Concurrent::ScheduledTask.execute(2){ 'What do you get when you multiply 6 by 9?' }
-
# task.state #=> pending
-
#
-
# @example Failed task execution
-
#
-
# task = Concurrent::ScheduledTask.execute(2){ raise StandardError.new('Call me maybe?') }
-
# task.pending? #=> true
-
#
-
# # wait for it...
-
# sleep(3)
-
#
-
# task.unscheduled? #=> false
-
# task.pending? #=> false
-
# task.fulfilled? #=> false
-
# task.rejected? #=> true
-
# task.value #=> nil
-
# task.reason #=> #<StandardError: Call me maybe?>
-
#
-
# @example Task execution with observation
-
#
-
# observer = Class.new{
-
# def update(time, value, reason)
-
# puts "The task completed at #{time} with value '#{value}'"
-
# end
-
# }.new
-
#
-
# task = Concurrent::ScheduledTask.new(2){ 'What does the fox say?' }
-
# task.add_observer(observer)
-
# task.execute
-
# task.pending? #=> true
-
#
-
# # wait for it...
-
# sleep(3)
-
#
-
# #>> The task completed at 2013-11-07 12:26:09 -0500 with value 'What does the fox say?'
-
#
-
# @!macro monotonic_clock_warning
-
#
-
# @see Concurrent.timer
-
1
class ScheduledTask < IVar
-
1
include Comparable
-
-
# The executor on which to execute the task.
-
# @!visibility private
-
1
attr_reader :executor
-
-
# Schedule a task for execution at a specified future time.
-
#
-
# @param [Float] delay the number of seconds to wait for before executing the task
-
#
-
# @yield the task to be performed
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @option opts [object, Array] :args zero or more arguments to be passed the task
-
# block on execution
-
#
-
# @raise [ArgumentError] When no block is given
-
# @raise [ArgumentError] When given a time that is in the past
-
1
def initialize(delay, opts = {}, &task)
-
raise ArgumentError.new('no block given') unless block_given?
-
raise ArgumentError.new('seconds must be greater than zero') if delay.to_f < 0.0
-
-
super(NULL, opts, &nil)
-
-
synchronize do
-
ns_set_state(:unscheduled)
-
@parent = opts.fetch(:timer_set, Concurrent.global_timer_set)
-
@args = get_arguments_from(opts)
-
@delay = delay.to_f
-
@task = task
-
@time = nil
-
@executor = Options.executor_from_options(opts) || Concurrent.global_io_executor
-
self.observers = Collection::CopyOnNotifyObserverSet.new
-
end
-
end
-
-
# The `delay` value given at instanciation.
-
#
-
# @return [Float] the initial delay.
-
1
def initial_delay
-
synchronize { @delay }
-
end
-
-
# The monotonic time at which the the task is scheduled to be executed.
-
#
-
# @return [Float] the schedule time or nil if `unscheduled`
-
1
def schedule_time
-
synchronize { @time }
-
end
-
-
# Comparator which orders by schedule time.
-
#
-
# @!visibility private
-
1
def <=>(other)
-
schedule_time <=> other.schedule_time
-
end
-
-
# Has the task been cancelled?
-
#
-
# @return [Boolean] true if the task is in the given state else false
-
1
def cancelled?
-
synchronize { ns_check_state?(:cancelled) }
-
end
-
-
# In the task execution in progress?
-
#
-
# @return [Boolean] true if the task is in the given state else false
-
1
def processing?
-
synchronize { ns_check_state?(:processing) }
-
end
-
-
# Cancel this task and prevent it from executing. A task can only be
-
# cancelled if it is pending or unscheduled.
-
#
-
# @return [Boolean] true if successfully cancelled else false
-
1
def cancel
-
if compare_and_set_state(:cancelled, :pending, :unscheduled)
-
complete(false, nil, CancelledOperationError.new)
-
# To avoid deadlocks this call must occur outside of #synchronize
-
# Changing the state above should prevent redundant calls
-
@parent.send(:remove_task, self)
-
else
-
false
-
end
-
end
-
-
# Reschedule the task using the original delay and the current time.
-
# A task can only be reset while it is `:pending`.
-
#
-
# @return [Boolean] true if successfully rescheduled else false
-
1
def reset
-
synchronize{ ns_reschedule(@delay) }
-
end
-
-
# Reschedule the task using the given delay and the current time.
-
# A task can only be reset while it is `:pending`.
-
#
-
# @param [Float] delay the number of seconds to wait for before executing the task
-
#
-
# @return [Boolean] true if successfully rescheduled else false
-
#
-
# @raise [ArgumentError] When given a time that is in the past
-
1
def reschedule(delay)
-
delay = delay.to_f
-
raise ArgumentError.new('seconds must be greater than zero') if delay < 0.0
-
synchronize{ ns_reschedule(delay) }
-
end
-
-
# Execute an `:unscheduled` `ScheduledTask`. Immediately sets the state to `:pending`
-
# and starts counting down toward execution. Does nothing if the `ScheduledTask` is
-
# in any state other than `:unscheduled`.
-
#
-
# @return [ScheduledTask] a reference to `self`
-
1
def execute
-
if compare_and_set_state(:pending, :unscheduled)
-
synchronize{ ns_schedule(@delay) }
-
end
-
self
-
end
-
-
# Create a new `ScheduledTask` object with the given block, execute it, and return the
-
# `:pending` object.
-
#
-
# @param [Float] delay the number of seconds to wait for before executing the task
-
#
-
# @!macro executor_and_deref_options
-
#
-
# @return [ScheduledTask] the newly created `ScheduledTask` in the `:pending` state
-
#
-
# @raise [ArgumentError] if no block is given
-
1
def self.execute(delay, opts = {}, &task)
-
new(delay, opts, &task).execute
-
end
-
-
# Execute the task.
-
#
-
# @!visibility private
-
1
def process_task
-
safe_execute(@task, @args)
-
end
-
-
1
protected :set, :try_set, :fail, :complete
-
-
1
protected
-
-
# Schedule the task using the given delay and the current time.
-
#
-
# @param [Float] delay the number of seconds to wait for before executing the task
-
#
-
# @return [Boolean] true if successfully rescheduled else false
-
#
-
# @!visibility private
-
1
def ns_schedule(delay)
-
@delay = delay
-
@time = Concurrent.monotonic_time + @delay
-
@parent.send(:post_task, self)
-
end
-
-
# Reschedule the task using the given delay and the current time.
-
# A task can only be reset while it is `:pending`.
-
#
-
# @param [Float] delay the number of seconds to wait for before executing the task
-
#
-
# @return [Boolean] true if successfully rescheduled else false
-
#
-
# @!visibility private
-
1
def ns_reschedule(delay)
-
return false unless ns_check_state?(:pending)
-
@parent.send(:remove_task, self) && ns_schedule(delay)
-
end
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
1
require 'concurrent/thread_safe/util'
-
1
require 'set'
-
-
1
module Concurrent
-
-
# @!macro concurrent_set
-
#
-
# A thread-safe subclass of Set. This version locks against the object
-
# itself for every method call, ensuring only one thread can be reading
-
# or writing at a time. This includes iteration methods like `#each`.
-
#
-
# @note `a += b` is **not** a **thread-safe** operation on
-
# `Concurrent::Set`. It reads Set `a`, then it creates new `Concurrent::Set`
-
# which is union of `a` and `b`, then it writes the union to `a`.
-
# The read and write are independent operations they do not form a single atomic
-
# operation therefore when two `+=` operations are executed concurrently updates
-
# may be lost. Use `#merge` instead.
-
#
-
# @see http://ruby-doc.org/stdlib-2.4.0/libdoc/set/rdoc/Set.html Ruby standard library `Set`
-
-
-
# @!macro internal_implementation_note
-
SetImplementation = case
-
1
when Concurrent.on_cruby?
-
# Because MRI never runs code in parallel, the existing
-
# non-thread-safe structures should usually work fine.
-
1
::Set
-
-
when Concurrent.on_jruby?
-
require 'jruby/synchronized'
-
-
class JRubySet < ::Set
-
include JRuby::Synchronized
-
end
-
JRubySet
-
-
when Concurrent.on_rbx?
-
require 'monitor'
-
require 'concurrent/thread_safe/util/data_structures'
-
-
class RbxSet < ::Set
-
end
-
ThreadSafe::Util.make_synchronized_on_rbx Concurrent::RbxSet
-
RbxSet
-
-
when Concurrent.on_truffleruby?
-
require 'concurrent/thread_safe/util/data_structures'
-
-
class TruffleRubySet < ::Set
-
end
-
-
ThreadSafe::Util.make_synchronized_on_truffleruby Concurrent::TruffleRubySet
-
TruffleRubySet
-
-
else
-
warn 'Possibly unsupported Ruby implementation'
-
::Set
-
end
-
1
private_constant :SetImplementation
-
-
# @!macro concurrent_set
-
1
class Set < SetImplementation
-
end
-
end
-
-
1
require 'concurrent/synchronization/abstract_struct'
-
1
require 'concurrent/errors'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# An thread-safe, write-once variation of Ruby's standard `Struct`.
-
# Each member can have its value set at most once, either at construction
-
# or any time thereafter. Attempting to assign a value to a member
-
# that has already been set will result in a `Concurrent::ImmutabilityError`.
-
#
-
# @see http://ruby-doc.org/core-2.2.0/Struct.html Ruby standard library `Struct`
-
# @see http://en.wikipedia.org/wiki/Final_(Java) Java `final` keyword
-
1
module SettableStruct
-
1
include Synchronization::AbstractStruct
-
-
# @!macro struct_values
-
1
def values
-
synchronize { ns_values }
-
end
-
1
alias_method :to_a, :values
-
-
# @!macro struct_values_at
-
1
def values_at(*indexes)
-
synchronize { ns_values_at(indexes) }
-
end
-
-
# @!macro struct_inspect
-
1
def inspect
-
synchronize { ns_inspect }
-
end
-
1
alias_method :to_s, :inspect
-
-
# @!macro struct_merge
-
1
def merge(other, &block)
-
synchronize { ns_merge(other, &block) }
-
end
-
-
# @!macro struct_to_h
-
1
def to_h
-
synchronize { ns_to_h }
-
end
-
-
# @!macro struct_get
-
1
def [](member)
-
synchronize { ns_get(member) }
-
end
-
-
# @!macro struct_equality
-
1
def ==(other)
-
synchronize { ns_equality(other) }
-
end
-
-
# @!macro struct_each
-
1
def each(&block)
-
return enum_for(:each) unless block_given?
-
synchronize { ns_each(&block) }
-
end
-
-
# @!macro struct_each_pair
-
1
def each_pair(&block)
-
return enum_for(:each_pair) unless block_given?
-
synchronize { ns_each_pair(&block) }
-
end
-
-
# @!macro struct_select
-
1
def select(&block)
-
return enum_for(:select) unless block_given?
-
synchronize { ns_select(&block) }
-
end
-
-
# @!macro struct_set
-
#
-
# @raise [Concurrent::ImmutabilityError] if the given member has already been set
-
1
def []=(member, value)
-
if member.is_a? Integer
-
length = synchronize { @values.length }
-
if member >= length
-
raise IndexError.new("offset #{member} too large for struct(size:#{length})")
-
end
-
synchronize do
-
unless @values[member].nil?
-
raise Concurrent::ImmutabilityError.new('struct member has already been set')
-
end
-
@values[member] = value
-
end
-
else
-
send("#{member}=", value)
-
end
-
rescue NoMethodError
-
raise NameError.new("no member '#{member}' in struct")
-
end
-
-
1
private
-
-
# @!visibility private
-
1
def initialize_copy(original)
-
synchronize do
-
super(original)
-
ns_initialize_copy
-
end
-
end
-
-
# @!macro struct_new
-
1
def self.new(*args, &block)
-
clazz_name = nil
-
if args.length == 0
-
raise ArgumentError.new('wrong number of arguments (0 for 1+)')
-
elsif args.length > 0 && args.first.is_a?(String)
-
clazz_name = args.shift
-
end
-
FACTORY.define_struct(clazz_name, args, &block)
-
end
-
-
1
FACTORY = Class.new(Synchronization::LockableObject) do
-
1
def define_struct(name, members, &block)
-
synchronize do
-
clazz = Synchronization::AbstractStruct.define_struct_class(SettableStruct, Synchronization::LockableObject, name, members, &block)
-
members.each_with_index do |member, index|
-
clazz.send :remove_method, member if clazz.instance_methods.include? member
-
clazz.send(:define_method, member) do
-
synchronize { @values[index] }
-
end
-
clazz.send(:define_method, "#{member}=") do |value|
-
synchronize do
-
unless @values[index].nil?
-
raise Concurrent::ImmutabilityError.new('struct member has already been set')
-
end
-
@values[index] = value
-
end
-
end
-
end
-
clazz
-
end
-
end
-
end.new
-
1
private_constant :FACTORY
-
end
-
end
-
1
require 'concurrent/utility/engine'
-
-
1
require 'concurrent/synchronization/abstract_object'
-
1
require 'concurrent/utility/native_extension_loader' # load native parts first
-
1
Concurrent.load_native_extensions
-
-
1
require 'concurrent/synchronization/mri_object'
-
1
require 'concurrent/synchronization/jruby_object'
-
1
require 'concurrent/synchronization/rbx_object'
-
1
require 'concurrent/synchronization/truffleruby_object'
-
1
require 'concurrent/synchronization/object'
-
1
require 'concurrent/synchronization/volatile'
-
-
1
require 'concurrent/synchronization/abstract_lockable_object'
-
1
require 'concurrent/synchronization/mutex_lockable_object'
-
1
require 'concurrent/synchronization/jruby_lockable_object'
-
1
require 'concurrent/synchronization/rbx_lockable_object'
-
-
1
require 'concurrent/synchronization/lockable_object'
-
-
1
require 'concurrent/synchronization/condition'
-
1
require 'concurrent/synchronization/lock'
-
-
1
module Concurrent
-
# {include:file:docs-source/synchronization.md}
-
# {include:file:docs-source/synchronization-notes.md}
-
1
module Synchronization
-
end
-
end
-
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
1
class AbstractLockableObject < Synchronization::Object
-
-
1
protected
-
-
# @!macro synchronization_object_method_synchronize
-
#
-
# @yield runs the block synchronized against this object,
-
# equivalent of java's `synchronize(this) {}`
-
# @note can by made public in descendants if required by `public :synchronize`
-
1
def synchronize
-
raise NotImplementedError
-
end
-
-
# @!macro synchronization_object_method_ns_wait_until
-
#
-
# Wait until condition is met or timeout passes,
-
# protects against spurious wake-ups.
-
# @param [Numeric, nil] timeout in seconds, `nil` means no timeout
-
# @yield condition to be met
-
# @yieldreturn [true, false]
-
# @return [true, false] if condition met
-
# @note only to be used inside synchronized block
-
# @note to provide direct access to this method in a descendant add method
-
# ```
-
# def wait_until(timeout = nil, &condition)
-
# synchronize { ns_wait_until(timeout, &condition) }
-
# end
-
# ```
-
1
def ns_wait_until(timeout = nil, &condition)
-
if timeout
-
wait_until = Concurrent.monotonic_time + timeout
-
loop do
-
now = Concurrent.monotonic_time
-
condition_result = condition.call
-
return condition_result if now >= wait_until || condition_result
-
ns_wait wait_until - now
-
end
-
else
-
ns_wait timeout until condition.call
-
true
-
end
-
end
-
-
# @!macro synchronization_object_method_ns_wait
-
#
-
# Wait until another thread calls #signal or #broadcast,
-
# spurious wake-ups can happen.
-
#
-
# @param [Numeric, nil] timeout in seconds, `nil` means no timeout
-
# @return [self]
-
# @note only to be used inside synchronized block
-
# @note to provide direct access to this method in a descendant add method
-
# ```
-
# def wait(timeout = nil)
-
# synchronize { ns_wait(timeout) }
-
# end
-
# ```
-
1
def ns_wait(timeout = nil)
-
raise NotImplementedError
-
end
-
-
# @!macro synchronization_object_method_ns_signal
-
#
-
# Signal one waiting thread.
-
# @return [self]
-
# @note only to be used inside synchronized block
-
# @note to provide direct access to this method in a descendant add method
-
# ```
-
# def signal
-
# synchronize { ns_signal }
-
# end
-
# ```
-
1
def ns_signal
-
raise NotImplementedError
-
end
-
-
# @!macro synchronization_object_method_ns_broadcast
-
#
-
# Broadcast to all waiting threads.
-
# @return [self]
-
# @note only to be used inside synchronized block
-
# @note to provide direct access to this method in a descendant add method
-
# ```
-
# def broadcast
-
# synchronize { ns_broadcast }
-
# end
-
# ```
-
1
def ns_broadcast
-
raise NotImplementedError
-
end
-
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class AbstractObject
-
-
# @abstract has to be implemented based on Ruby runtime
-
1
def initialize
-
raise NotImplementedError
-
end
-
-
# @!visibility private
-
# @abstract
-
1
def full_memory_barrier
-
raise NotImplementedError
-
end
-
-
1
def self.attr_volatile(*names)
-
raise NotImplementedError
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
module AbstractStruct
-
-
# @!visibility private
-
1
def initialize(*values)
-
super()
-
ns_initialize(*values)
-
end
-
-
# @!macro struct_length
-
#
-
# Returns the number of struct members.
-
#
-
# @return [Fixnum] the number of struct members
-
1
def length
-
self.class::MEMBERS.length
-
end
-
1
alias_method :size, :length
-
-
# @!macro struct_members
-
#
-
# Returns the struct members as an array of symbols.
-
#
-
# @return [Array] the struct members as an array of symbols
-
1
def members
-
self.class::MEMBERS.dup
-
end
-
-
1
protected
-
-
# @!macro struct_values
-
#
-
# @!visibility private
-
1
def ns_values
-
@values.dup
-
end
-
-
# @!macro struct_values_at
-
#
-
# @!visibility private
-
1
def ns_values_at(indexes)
-
@values.values_at(*indexes)
-
end
-
-
# @!macro struct_to_h
-
#
-
# @!visibility private
-
1
def ns_to_h
-
length.times.reduce({}){|memo, i| memo[self.class::MEMBERS[i]] = @values[i]; memo}
-
end
-
-
# @!macro struct_get
-
#
-
# @!visibility private
-
1
def ns_get(member)
-
if member.is_a? Integer
-
if member >= @values.length
-
raise IndexError.new("offset #{member} too large for struct(size:#{@values.length})")
-
end
-
@values[member]
-
else
-
send(member)
-
end
-
rescue NoMethodError
-
raise NameError.new("no member '#{member}' in struct")
-
end
-
-
# @!macro struct_equality
-
#
-
# @!visibility private
-
1
def ns_equality(other)
-
self.class == other.class && self.values == other.values
-
end
-
-
# @!macro struct_each
-
#
-
# @!visibility private
-
1
def ns_each
-
values.each{|value| yield value }
-
end
-
-
# @!macro struct_each_pair
-
#
-
# @!visibility private
-
1
def ns_each_pair
-
@values.length.times do |index|
-
yield self.class::MEMBERS[index], @values[index]
-
end
-
end
-
-
# @!macro struct_select
-
#
-
# @!visibility private
-
1
def ns_select
-
values.select{|value| yield value }
-
end
-
-
# @!macro struct_inspect
-
#
-
# @!visibility private
-
1
def ns_inspect
-
struct = pr_underscore(self.class.ancestors[1])
-
clazz = ((self.class.to_s =~ /^#<Class:/) == 0) ? '' : " #{self.class}"
-
"#<#{struct}#{clazz} #{ns_to_h}>"
-
end
-
-
# @!macro struct_merge
-
#
-
# @!visibility private
-
1
def ns_merge(other, &block)
-
self.class.new(*self.to_h.merge(other, &block).values)
-
end
-
-
# @!visibility private
-
1
def ns_initialize_copy
-
@values = @values.map do |val|
-
begin
-
val.clone
-
rescue TypeError
-
val
-
end
-
end
-
end
-
-
# @!visibility private
-
1
def pr_underscore(clazz)
-
word = clazz.to_s.dup # dup string to workaround JRuby 9.2.0.0 bug https://github.com/jruby/jruby/issues/5229
-
word.gsub!(/::/, '/')
-
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
-
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
-
word.tr!("-", "_")
-
word.downcase!
-
word
-
end
-
-
# @!visibility private
-
1
def self.define_struct_class(parent, base, name, members, &block)
-
clazz = Class.new(base || Object) do
-
include parent
-
self.const_set(:MEMBERS, members.collect{|member| member.to_s.to_sym}.freeze)
-
def ns_initialize(*values)
-
raise ArgumentError.new('struct size differs') if values.length > length
-
@values = values.fill(nil, values.length..length-1)
-
end
-
end
-
unless name.nil?
-
begin
-
parent.send :remove_const, name if parent.const_defined?(name, false)
-
parent.const_set(name, clazz)
-
clazz
-
rescue NameError
-
raise NameError.new("identifier #{name} needs to be constant")
-
end
-
end
-
members.each_with_index do |member, index|
-
clazz.send :remove_method, member if clazz.instance_methods.include? member
-
clazz.send(:define_method, member) do
-
@values[index]
-
end
-
end
-
clazz.class_exec(&block) unless block.nil?
-
clazz.singleton_class.send :alias_method, :[], :new
-
clazz
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
# TODO (pitr-ch 04-Dec-2016): should be in edge
-
1
class Condition < LockableObject
-
1
safe_initialization!
-
-
# TODO (pitr 12-Sep-2015): locks two objects, improve
-
# TODO (pitr 26-Sep-2015): study
-
# http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8-b132/java/util/concurrent/locks/AbstractQueuedSynchronizer.java#AbstractQueuedSynchronizer.Node
-
-
1
singleton_class.send :alias_method, :private_new, :new
-
1
private_class_method :new
-
-
1
def initialize(lock)
-
super()
-
@Lock = lock
-
end
-
-
1
def wait(timeout = nil)
-
@Lock.synchronize { ns_wait(timeout) }
-
end
-
-
1
def ns_wait(timeout = nil)
-
synchronize { super(timeout) }
-
end
-
-
1
def wait_until(timeout = nil, &condition)
-
@Lock.synchronize { ns_wait_until(timeout, &condition) }
-
end
-
-
1
def ns_wait_until(timeout = nil, &condition)
-
synchronize { super(timeout, &condition) }
-
end
-
-
1
def signal
-
@Lock.synchronize { ns_signal }
-
end
-
-
1
def ns_signal
-
synchronize { super }
-
end
-
-
1
def broadcast
-
@Lock.synchronize { ns_broadcast }
-
end
-
-
1
def ns_broadcast
-
synchronize { super }
-
end
-
end
-
-
1
class LockableObject < LockableObjectImplementation
-
1
def new_condition
-
Condition.private_new(self)
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
1
if Concurrent.on_jruby? && Concurrent.java_extensions_loaded?
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
class JRubyLockableObject < AbstractLockableObject
-
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
1
if Concurrent.on_jruby? && Concurrent.java_extensions_loaded?
-
-
# @!visibility private
-
module JRubyAttrVolatile
-
def self.included(base)
-
base.extend(ClassMethods)
-
end
-
-
module ClassMethods
-
def attr_volatile(*names)
-
names.each do |name|
-
-
ivar = :"@volatile_#{name}"
-
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{name}
-
instance_variable_get_volatile(:#{ivar})
-
end
-
-
def #{name}=(value)
-
instance_variable_set_volatile(:#{ivar}, value)
-
end
-
RUBY
-
-
end
-
names.map { |n| [n, :"#{n}="] }.flatten
-
end
-
end
-
end
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
class JRubyObject < AbstractObject
-
include JRubyAttrVolatile
-
-
def initialize
-
# nothing to do
-
end
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
# TODO (pitr-ch 04-Dec-2016): should be in edge
-
1
class Lock < LockableObject
-
# TODO use JavaReentrantLock on JRuby
-
-
1
public :synchronize
-
-
1
def wait(timeout = nil)
-
synchronize { ns_wait(timeout) }
-
end
-
-
1
public :ns_wait
-
-
1
def wait_until(timeout = nil, &condition)
-
synchronize { ns_wait_until(timeout, &condition) }
-
end
-
-
1
public :ns_wait_until
-
-
1
def signal
-
synchronize { ns_signal }
-
end
-
-
1
public :ns_signal
-
-
1
def broadcast
-
synchronize { ns_broadcast }
-
end
-
-
1
public :ns_broadcast
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
LockableObjectImplementation = case
-
1
when Concurrent.on_cruby? && Concurrent.ruby_version(:<=, 1, 9, 3)
-
MonitorLockableObject
-
when Concurrent.on_cruby? && Concurrent.ruby_version(:>, 1, 9, 3)
-
1
MutexLockableObject
-
when Concurrent.on_jruby?
-
JRubyLockableObject
-
when Concurrent.on_rbx?
-
RbxLockableObject
-
when Concurrent.on_truffleruby?
-
MutexLockableObject
-
else
-
warn 'Possibly unsupported Ruby implementation'
-
MonitorLockableObject
-
end
-
1
private_constant :LockableObjectImplementation
-
-
# Safe synchronization under any Ruby implementation.
-
# It provides methods like {#synchronize}, {#wait}, {#signal} and {#broadcast}.
-
# Provides a single layer which can improve its implementation over time without changes needed to
-
# the classes using it. Use {Synchronization::Object} not this abstract class.
-
#
-
# @note this object does not support usage together with
-
# [`Thread#wakeup`](http://ruby-doc.org/core-2.2.0/Thread.html#method-i-wakeup)
-
# and [`Thread#raise`](http://ruby-doc.org/core-2.2.0/Thread.html#method-i-raise).
-
# `Thread#sleep` and `Thread#wakeup` will work as expected but mixing `Synchronization::Object#wait` and
-
# `Thread#wakeup` will not work on all platforms.
-
#
-
# @see Event implementation as an example of this class use
-
#
-
# @example simple
-
# class AnClass < Synchronization::Object
-
# def initialize
-
# super
-
# synchronize { @value = 'asd' }
-
# end
-
#
-
# def value
-
# synchronize { @value }
-
# end
-
# end
-
#
-
# @!visibility private
-
1
class LockableObject < LockableObjectImplementation
-
-
# TODO (pitr 12-Sep-2015): make private for c-r, prohibit subclassing
-
# TODO (pitr 12-Sep-2015): we inherit too much ourselves :/
-
-
# @!method initialize(*args, &block)
-
# @!macro synchronization_object_method_initialize
-
-
# @!method synchronize
-
# @!macro synchronization_object_method_synchronize
-
-
# @!method wait_until(timeout = nil, &condition)
-
# @!macro synchronization_object_method_ns_wait_until
-
-
# @!method wait(timeout = nil)
-
# @!macro synchronization_object_method_ns_wait
-
-
# @!method signal
-
# @!macro synchronization_object_method_ns_signal
-
-
# @!method broadcast
-
# @!macro synchronization_object_method_ns_broadcast
-
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
1
module MriAttrVolatile
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
1
module ClassMethods
-
1
def attr_volatile(*names)
-
names.each do |name|
-
ivar = :"@volatile_#{name}"
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{name}
-
#{ivar}
-
end
-
-
def #{name}=(value)
-
#{ivar} = value
-
end
-
RUBY
-
end
-
names.map { |n| [n, :"#{n}="] }.flatten
-
end
-
end
-
-
1
def full_memory_barrier
-
# relying on undocumented behavior of CRuby, GVL acquire has lock which ensures visibility of ivars
-
# https://github.com/ruby/ruby/blob/ruby_2_2/thread_pthread.c#L204-L211
-
end
-
end
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class MriObject < AbstractObject
-
1
include MriAttrVolatile
-
-
1
def initialize
-
# nothing to do
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
# noinspection RubyInstanceVariableNamingConvention
-
1
module Synchronization
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
module ConditionSignalling
-
1
protected
-
-
1
def ns_signal
-
@__Condition__.signal
-
self
-
end
-
-
1
def ns_broadcast
-
@__Condition__.broadcast
-
self
-
end
-
end
-
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class MutexLockableObject < AbstractLockableObject
-
1
include ConditionSignalling
-
-
1
safe_initialization!
-
-
1
def initialize(*defaults)
-
16
super(*defaults)
-
16
@__Lock__ = ::Mutex.new
-
16
@__Condition__ = ::ConditionVariable.new
-
end
-
-
1
protected
-
-
1
def synchronize
-
17
if @__Lock__.owned?
-
5
yield
-
else
-
24
@__Lock__.synchronize { yield }
-
end
-
end
-
-
1
def ns_wait(timeout = nil)
-
@__Condition__.wait @__Lock__, timeout
-
self
-
end
-
end
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class MonitorLockableObject < AbstractLockableObject
-
1
include ConditionSignalling
-
-
1
safe_initialization!
-
-
1
def initialize(*defaults)
-
super(*defaults)
-
@__Lock__ = ::Monitor.new
-
@__Condition__ = @__Lock__.new_cond
-
end
-
-
1
protected
-
-
1
def synchronize # TODO may be a problem with lock.synchronize { lock.wait }
-
@__Lock__.synchronize { yield }
-
end
-
-
1
def ns_wait(timeout = nil)
-
@__Condition__.wait timeout
-
self
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
ObjectImplementation = case
-
1
when Concurrent.on_cruby?
-
1
MriObject
-
when Concurrent.on_jruby?
-
JRubyObject
-
when Concurrent.on_rbx?
-
RbxObject
-
when Concurrent.on_truffleruby?
-
TruffleRubyObject
-
else
-
warn 'Possibly unsupported Ruby implementation'
-
MriObject
-
end
-
1
private_constant :ObjectImplementation
-
-
# Abstract object providing final, volatile, ans CAS extensions to build other concurrent abstractions.
-
# - final instance variables see {Object.safe_initialization!}
-
# - volatile instance variables see {Object.attr_volatile}
-
# - volatile instance variables see {Object.attr_atomic}
-
1
class Object < ObjectImplementation
-
# TODO make it a module if possible
-
-
# @!method self.attr_volatile(*names)
-
# Creates methods for reading and writing (as `attr_accessor` does) to a instance variable with
-
# volatile (Java) semantic. The instance variable should be accessed only through generated methods.
-
#
-
# @param [::Array<Symbol>] names of the instance variables to be volatile
-
# @return [::Array<Symbol>] names of defined method names
-
-
# Has to be called by children.
-
1
def initialize
-
16
super
-
16
__initialize_atomic_fields__
-
end
-
-
# By calling this method on a class, it and all its children are marked to be constructed safely. Meaning that
-
# all writes (ivar initializations) are made visible to all readers of newly constructed object. It ensures
-
# same behaviour as Java's final fields.
-
# @example
-
# class AClass < Concurrent::Synchronization::Object
-
# safe_initialization!
-
#
-
# def initialize
-
# @AFinalValue = 'value' # published safely, does not have to be synchronized
-
# end
-
# end
-
# @return [true]
-
1
def self.safe_initialization!
-
# define only once, and not again in children
-
23
return if safe_initialization?
-
-
# @!visibility private
-
15
def self.new(*args, &block)
-
17
object = super(*args, &block)
-
ensure
-
17
object.full_memory_barrier if object
-
end
-
-
15
@safe_initialization = true
-
end
-
-
# @return [true, false] if this class is safely initialized.
-
1
def self.safe_initialization?
-
49
@safe_initialization = false unless defined? @safe_initialization
-
49
@safe_initialization || (superclass.respond_to?(:safe_initialization?) && superclass.safe_initialization?)
-
end
-
-
# For testing purposes, quite slow. Injects assert code to new method which will raise if class instance contains
-
# any instance variables with CamelCase names and isn't {.safe_initialization?}.
-
# @raise when offend found
-
# @return [true]
-
1
def self.ensure_safe_initialization_when_final_fields_are_present
-
Object.class_eval do
-
def self.new(*args, &block)
-
object = super(*args, &block)
-
ensure
-
has_final_field = object.instance_variables.any? { |v| v.to_s =~ /^@[A-Z]/ }
-
if has_final_field && !safe_initialization?
-
raise "there was an instance of #{object.class} with final field but not marked with safe_initialization!"
-
end
-
end
-
end
-
true
-
end
-
-
# Creates methods for reading and writing to a instance variable with
-
# volatile (Java) semantic as {.attr_volatile} does.
-
# The instance variable should be accessed oly through generated methods.
-
# This method generates following methods: `value`, `value=(new_value) #=> new_value`,
-
# `swap_value(new_value) #=> old_value`,
-
# `compare_and_set_value(expected, value) #=> true || false`, `update_value(&block)`.
-
# @param [::Array<Symbol>] names of the instance variables to be volatile with CAS.
-
# @return [::Array<Symbol>] names of defined method names.
-
# @!macro attr_atomic
-
# @!method $1
-
# @return [Object] The $1.
-
# @!method $1=(new_$1)
-
# Set the $1.
-
# @return [Object] new_$1.
-
# @!method swap_$1(new_$1)
-
# Set the $1 to new_$1 and return the old $1.
-
# @return [Object] old $1
-
# @!method compare_and_set_$1(expected_$1, new_$1)
-
# Sets the $1 to new_$1 if the current $1 is expected_$1
-
# @return [true, false]
-
# @!method update_$1(&block)
-
# Updates the $1 using the block.
-
# @yield [Object] Calculate a new $1 using given (old) $1
-
# @yieldparam [Object] old $1
-
# @return [Object] new $1
-
1
def self.attr_atomic(*names)
-
6
@__atomic_fields__ ||= []
-
6
@__atomic_fields__ += names
-
6
safe_initialization!
-
6
define_initialize_atomic_fields
-
-
6
names.each do |name|
-
13
ivar = :"@Atomic#{name.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }}"
-
6
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{name}
-
#{ivar}.get
-
end
-
-
def #{name}=(value)
-
#{ivar}.set value
-
end
-
-
def swap_#{name}(value)
-
#{ivar}.swap value
-
end
-
-
def compare_and_set_#{name}(expected, value)
-
#{ivar}.compare_and_set expected, value
-
end
-
-
def update_#{name}(&block)
-
#{ivar}.update(&block)
-
end
-
RUBY
-
end
-
12
names.flat_map { |n| [n, :"#{n}=", :"swap_#{n}", :"compare_and_set_#{n}", :"update_#{n}"] }
-
end
-
-
# @param [true, false] inherited should inherited volatile with CAS fields be returned?
-
# @return [::Array<Symbol>] Returns defined volatile with CAS fields on this class.
-
1
def self.atomic_attributes(inherited = true)
-
@__atomic_fields__ ||= []
-
((superclass.atomic_attributes if superclass.respond_to?(:atomic_attributes) && inherited) || []) + @__atomic_fields__
-
end
-
-
# @return [true, false] is the attribute with name atomic?
-
1
def self.atomic_attribute?(name)
-
atomic_attributes.include? name
-
end
-
-
1
private
-
-
1
def self.define_initialize_atomic_fields
-
6
assignments = @__atomic_fields__.map do |name|
-
13
"@Atomic#{name.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }} = Concurrent::AtomicReference.new(nil)"
-
end.join("\n")
-
-
6
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def __initialize_atomic_fields__
-
super
-
#{assignments}
-
end
-
RUBY
-
end
-
-
1
private_class_method :define_initialize_atomic_fields
-
-
1
def __initialize_atomic_fields__
-
end
-
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class RbxLockableObject < AbstractLockableObject
-
1
safe_initialization!
-
-
1
def initialize(*defaults)
-
super(*defaults)
-
@__Waiters__ = []
-
@__owner__ = nil
-
end
-
-
1
protected
-
-
1
def synchronize(&block)
-
if @__owner__ == Thread.current
-
yield
-
else
-
result = nil
-
Rubinius.synchronize(self) do
-
begin
-
@__owner__ = Thread.current
-
result = yield
-
ensure
-
@__owner__ = nil
-
end
-
end
-
result
-
end
-
end
-
-
1
def ns_wait(timeout = nil)
-
wchan = Rubinius::Channel.new
-
-
begin
-
@__Waiters__.push wchan
-
Rubinius.unlock(self)
-
signaled = wchan.receive_timeout timeout
-
ensure
-
Rubinius.lock(self)
-
-
if !signaled && !@__Waiters__.delete(wchan)
-
# we timed out, but got signaled afterwards,
-
# so pass that signal on to the next waiter
-
@__Waiters__.shift << true unless @__Waiters__.empty?
-
end
-
end
-
-
self
-
end
-
-
1
def ns_signal
-
@__Waiters__.shift << true unless @__Waiters__.empty?
-
self
-
end
-
-
1
def ns_broadcast
-
@__Waiters__.shift << true until @__Waiters__.empty?
-
self
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
1
module RbxAttrVolatile
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
1
module ClassMethods
-
-
1
def attr_volatile(*names)
-
names.each do |name|
-
ivar = :"@volatile_#{name}"
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{name}
-
Rubinius.memory_barrier
-
#{ivar}
-
end
-
-
def #{name}=(value)
-
#{ivar} = value
-
Rubinius.memory_barrier
-
end
-
RUBY
-
end
-
names.map { |n| [n, :"#{n}="] }.flatten
-
end
-
-
end
-
-
1
def full_memory_barrier
-
# Rubinius instance variables are not volatile so we need to insert barrier
-
# TODO (pitr 26-Nov-2015): check comments like ^
-
Rubinius.memory_barrier
-
end
-
end
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class RbxObject < AbstractObject
-
1
include RbxAttrVolatile
-
-
1
def initialize
-
# nothing to do
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# @!visibility private
-
1
module TruffleRubyAttrVolatile
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
1
module ClassMethods
-
1
def attr_volatile(*names)
-
names.each do |name|
-
ivar = :"@volatile_#{name}"
-
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
-
def #{name}
-
full_memory_barrier
-
#{ivar}
-
end
-
-
def #{name}=(value)
-
#{ivar} = value
-
full_memory_barrier
-
end
-
RUBY
-
end
-
-
names.map { |n| [n, :"#{n}="] }.flatten
-
end
-
end
-
-
1
def full_memory_barrier
-
TruffleRuby.full_memory_barrier
-
end
-
end
-
-
# @!visibility private
-
# @!macro internal_implementation_note
-
1
class TruffleRubyObject < AbstractObject
-
1
include TruffleRubyAttrVolatile
-
-
1
def initialize
-
# nothing to do
-
end
-
end
-
end
-
end
-
1
module Concurrent
-
1
module Synchronization
-
-
# Volatile adds the attr_volatile class method when included.
-
#
-
# @example
-
# class Foo
-
# include Concurrent::Synchronization::Volatile
-
#
-
# attr_volatile :bar
-
#
-
# def initialize
-
# self.bar = 1
-
# end
-
# end
-
#
-
# foo = Foo.new
-
# foo.bar
-
# => 1
-
# foo.bar = 2
-
# => 2
-
-
Volatile = case
-
1
when Concurrent.on_cruby?
-
1
MriAttrVolatile
-
when Concurrent.on_jruby?
-
JRubyAttrVolatile
-
when Concurrent.on_rbx?
-
RbxAttrVolatile
-
when Concurrent.on_truffleruby?
-
TruffleRubyAttrVolatile
-
else
-
MriAttrVolatile
-
end
-
end
-
end
-
1
require 'delegate'
-
1
require 'monitor'
-
-
1
module Concurrent
-
1
unless defined?(SynchronizedDelegator)
-
-
# This class provides a trivial way to synchronize all calls to a given object
-
# by wrapping it with a `Delegator` that performs `Monitor#enter/exit` calls
-
# around the delegated `#send`. Example:
-
#
-
# array = [] # not thread-safe on many impls
-
# array = SynchronizedDelegator.new([]) # thread-safe
-
#
-
# A simple `Monitor` provides a very coarse-grained way to synchronize a given
-
# object, in that it will cause synchronization for methods that have no need
-
# for it, but this is a trivial way to get thread-safety where none may exist
-
# currently on some implementations.
-
#
-
# This class is currently being considered for inclusion into stdlib, via
-
# https://bugs.ruby-lang.org/issues/8556
-
#
-
# @!visibility private
-
1
class SynchronizedDelegator < SimpleDelegator
-
1
def setup
-
@old_abort = Thread.abort_on_exception
-
Thread.abort_on_exception = true
-
end
-
-
1
def teardown
-
Thread.abort_on_exception = @old_abort
-
end
-
-
1
def initialize(obj)
-
__setobj__(obj)
-
@monitor = Monitor.new
-
end
-
-
1
def method_missing(method, *args, &block)
-
monitor = @monitor
-
begin
-
monitor.enter
-
super
-
ensure
-
monitor.exit
-
end
-
end
-
-
end
-
end
-
end
-
1
module Concurrent
-
-
# @!visibility private
-
1
module ThreadSafe
-
-
# @!visibility private
-
1
module Util
-
-
# TODO (pitr-ch 15-Oct-2016): migrate to Utility::NativeInteger
-
1
FIXNUM_BIT_SIZE = (0.size * 8) - 2
-
1
MAX_INT = (2 ** FIXNUM_BIT_SIZE) - 1
-
# TODO (pitr-ch 15-Oct-2016): migrate to Utility::ProcessorCounter
-
1
CPU_COUNT = 16 # is there a way to determine this?
-
end
-
end
-
end
-
1
require 'concurrent/collection/copy_on_notify_observer_set'
-
1
require 'concurrent/concern/dereferenceable'
-
1
require 'concurrent/concern/observable'
-
1
require 'concurrent/atomic/atomic_boolean'
-
1
require 'concurrent/executor/executor_service'
-
1
require 'concurrent/executor/ruby_executor_service'
-
1
require 'concurrent/executor/safe_task_executor'
-
1
require 'concurrent/scheduled_task'
-
-
1
module Concurrent
-
-
# A very common concurrency pattern is to run a thread that performs a task at
-
# regular intervals. The thread that performs the task sleeps for the given
-
# interval then wakes up and performs the task. Lather, rinse, repeat... This
-
# pattern causes two problems. First, it is difficult to test the business
-
# logic of the task because the task itself is tightly coupled with the
-
# concurrency logic. Second, an exception raised while performing the task can
-
# cause the entire thread to abend. In a long-running application where the
-
# task thread is intended to run for days/weeks/years a crashed task thread
-
# can pose a significant problem. `TimerTask` alleviates both problems.
-
#
-
# When a `TimerTask` is launched it starts a thread for monitoring the
-
# execution interval. The `TimerTask` thread does not perform the task,
-
# however. Instead, the TimerTask launches the task on a separate thread.
-
# Should the task experience an unrecoverable crash only the task thread will
-
# crash. This makes the `TimerTask` very fault tolerant. Additionally, the
-
# `TimerTask` thread can respond to the success or failure of the task,
-
# performing logging or ancillary operations. `TimerTask` can also be
-
# configured with a timeout value allowing it to kill a task that runs too
-
# long.
-
#
-
# One other advantage of `TimerTask` is that it forces the business logic to
-
# be completely decoupled from the concurrency logic. The business logic can
-
# be tested separately then passed to the `TimerTask` for scheduling and
-
# running.
-
#
-
# In some cases it may be necessary for a `TimerTask` to affect its own
-
# execution cycle. To facilitate this, a reference to the TimerTask instance
-
# is passed as an argument to the provided block every time the task is
-
# executed.
-
#
-
# The `TimerTask` class includes the `Dereferenceable` mixin module so the
-
# result of the last execution is always available via the `#value` method.
-
# Dereferencing options can be passed to the `TimerTask` during construction or
-
# at any later time using the `#set_deref_options` method.
-
#
-
# `TimerTask` supports notification through the Ruby standard library
-
# {http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html
-
# Observable} module. On execution the `TimerTask` will notify the observers
-
# with three arguments: time of execution, the result of the block (or nil on
-
# failure), and any raised exceptions (or nil on success). If the timeout
-
# interval is exceeded the observer will receive a `Concurrent::TimeoutError`
-
# object as the third argument.
-
#
-
# @!macro copy_options
-
#
-
# @example Basic usage
-
# task = Concurrent::TimerTask.new{ puts 'Boom!' }
-
# task.execute
-
#
-
# task.execution_interval #=> 60 (default)
-
# task.timeout_interval #=> 30 (default)
-
#
-
# # wait 60 seconds...
-
# #=> 'Boom!'
-
#
-
# task.shutdown #=> true
-
#
-
# @example Configuring `:execution_interval` and `:timeout_interval`
-
# task = Concurrent::TimerTask.new(execution_interval: 5, timeout_interval: 5) do
-
# puts 'Boom!'
-
# end
-
#
-
# task.execution_interval #=> 5
-
# task.timeout_interval #=> 5
-
#
-
# @example Immediate execution with `:run_now`
-
# task = Concurrent::TimerTask.new(run_now: true){ puts 'Boom!' }
-
# task.execute
-
#
-
# #=> 'Boom!'
-
#
-
# @example Last `#value` and `Dereferenceable` mixin
-
# task = Concurrent::TimerTask.new(
-
# dup_on_deref: true,
-
# execution_interval: 5
-
# ){ Time.now }
-
#
-
# task.execute
-
# Time.now #=> 2013-11-07 18:06:50 -0500
-
# sleep(10)
-
# task.value #=> 2013-11-07 18:06:55 -0500
-
#
-
# @example Controlling execution from within the block
-
# timer_task = Concurrent::TimerTask.new(execution_interval: 1) do |task|
-
# task.execution_interval.times{ print 'Boom! ' }
-
# print "\n"
-
# task.execution_interval += 1
-
# if task.execution_interval > 5
-
# puts 'Stopping...'
-
# task.shutdown
-
# end
-
# end
-
#
-
# timer_task.execute # blocking call - this task will stop itself
-
# #=> Boom!
-
# #=> Boom! Boom!
-
# #=> Boom! Boom! Boom!
-
# #=> Boom! Boom! Boom! Boom!
-
# #=> Boom! Boom! Boom! Boom! Boom!
-
# #=> Stopping...
-
#
-
# @example Observation
-
# class TaskObserver
-
# def update(time, result, ex)
-
# if result
-
# print "(#{time}) Execution successfully returned #{result}\n"
-
# elsif ex.is_a?(Concurrent::TimeoutError)
-
# print "(#{time}) Execution timed out\n"
-
# else
-
# print "(#{time}) Execution failed with error #{ex}\n"
-
# end
-
# end
-
# end
-
#
-
# task = Concurrent::TimerTask.new(execution_interval: 1, timeout_interval: 1){ 42 }
-
# task.add_observer(TaskObserver.new)
-
# task.execute
-
# sleep 4
-
#
-
# #=> (2013-10-13 19:08:58 -0400) Execution successfully returned 42
-
# #=> (2013-10-13 19:08:59 -0400) Execution successfully returned 42
-
# #=> (2013-10-13 19:09:00 -0400) Execution successfully returned 42
-
# task.shutdown
-
#
-
# task = Concurrent::TimerTask.new(execution_interval: 1, timeout_interval: 1){ sleep }
-
# task.add_observer(TaskObserver.new)
-
# task.execute
-
#
-
# #=> (2013-10-13 19:07:25 -0400) Execution timed out
-
# #=> (2013-10-13 19:07:27 -0400) Execution timed out
-
# #=> (2013-10-13 19:07:29 -0400) Execution timed out
-
# task.shutdown
-
#
-
# task = Concurrent::TimerTask.new(execution_interval: 1){ raise StandardError }
-
# task.add_observer(TaskObserver.new)
-
# task.execute
-
#
-
# #=> (2013-10-13 19:09:37 -0400) Execution failed with error StandardError
-
# #=> (2013-10-13 19:09:38 -0400) Execution failed with error StandardError
-
# #=> (2013-10-13 19:09:39 -0400) Execution failed with error StandardError
-
# task.shutdown
-
#
-
# @see http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html
-
# @see http://docs.oracle.com/javase/7/docs/api/java/util/TimerTask.html
-
1
class TimerTask < RubyExecutorService
-
1
include Concern::Dereferenceable
-
1
include Concern::Observable
-
-
# Default `:execution_interval` in seconds.
-
1
EXECUTION_INTERVAL = 60
-
-
# Default `:timeout_interval` in seconds.
-
1
TIMEOUT_INTERVAL = 30
-
-
# Create a new TimerTask with the given task and configuration.
-
#
-
# @!macro timer_task_initialize
-
# @param [Hash] opts the options defining task execution.
-
# @option opts [Integer] :execution_interval number of seconds between
-
# task executions (default: EXECUTION_INTERVAL)
-
# @option opts [Integer] :timeout_interval number of seconds a task can
-
# run before it is considered to have failed (default: TIMEOUT_INTERVAL)
-
# @option opts [Boolean] :run_now Whether to run the task immediately
-
# upon instantiation or to wait until the first # execution_interval
-
# has passed (default: false)
-
#
-
# @!macro deref_options
-
#
-
# @raise ArgumentError when no block is given.
-
#
-
# @yield to the block after :execution_interval seconds have passed since
-
# the last yield
-
# @yieldparam task a reference to the `TimerTask` instance so that the
-
# block can control its own lifecycle. Necessary since `self` will
-
# refer to the execution context of the block rather than the running
-
# `TimerTask`.
-
#
-
# @return [TimerTask] the new `TimerTask`
-
1
def initialize(opts = {}, &task)
-
raise ArgumentError.new('no block given') unless block_given?
-
super
-
set_deref_options opts
-
end
-
-
# Is the executor running?
-
#
-
# @return [Boolean] `true` when running, `false` when shutting down or shutdown
-
1
def running?
-
@running.true?
-
end
-
-
# Execute a previously created `TimerTask`.
-
#
-
# @return [TimerTask] a reference to `self`
-
#
-
# @example Instance and execute in separate steps
-
# task = Concurrent::TimerTask.new(execution_interval: 10){ print "Hello World\n" }
-
# task.running? #=> false
-
# task.execute
-
# task.running? #=> true
-
#
-
# @example Instance and execute in one line
-
# task = Concurrent::TimerTask.new(execution_interval: 10){ print "Hello World\n" }.execute
-
# task.running? #=> true
-
1
def execute
-
synchronize do
-
if @running.false?
-
@running.make_true
-
schedule_next_task(@run_now ? 0 : @execution_interval)
-
end
-
end
-
self
-
end
-
-
# Create and execute a new `TimerTask`.
-
#
-
# @!macro timer_task_initialize
-
#
-
# @example
-
# task = Concurrent::TimerTask.execute(execution_interval: 10){ print "Hello World\n" }
-
# task.running? #=> true
-
1
def self.execute(opts = {}, &task)
-
TimerTask.new(opts, &task).execute
-
end
-
-
# @!attribute [rw] execution_interval
-
# @return [Fixnum] Number of seconds after the task completes before the
-
# task is performed again.
-
1
def execution_interval
-
synchronize { @execution_interval }
-
end
-
-
# @!attribute [rw] execution_interval
-
# @return [Fixnum] Number of seconds after the task completes before the
-
# task is performed again.
-
1
def execution_interval=(value)
-
if (value = value.to_f) <= 0.0
-
raise ArgumentError.new('must be greater than zero')
-
else
-
synchronize { @execution_interval = value }
-
end
-
end
-
-
# @!attribute [rw] timeout_interval
-
# @return [Fixnum] Number of seconds the task can run before it is
-
# considered to have failed.
-
1
def timeout_interval
-
synchronize { @timeout_interval }
-
end
-
-
# @!attribute [rw] timeout_interval
-
# @return [Fixnum] Number of seconds the task can run before it is
-
# considered to have failed.
-
1
def timeout_interval=(value)
-
if (value = value.to_f) <= 0.0
-
raise ArgumentError.new('must be greater than zero')
-
else
-
synchronize { @timeout_interval = value }
-
end
-
end
-
-
1
private :post, :<<
-
-
1
private
-
-
1
def ns_initialize(opts, &task)
-
set_deref_options(opts)
-
-
self.execution_interval = opts[:execution] || opts[:execution_interval] || EXECUTION_INTERVAL
-
self.timeout_interval = opts[:timeout] || opts[:timeout_interval] || TIMEOUT_INTERVAL
-
@run_now = opts[:now] || opts[:run_now]
-
@executor = Concurrent::SafeTaskExecutor.new(task)
-
@running = Concurrent::AtomicBoolean.new(false)
-
@value = nil
-
-
self.observers = Collection::CopyOnNotifyObserverSet.new
-
end
-
-
# @!visibility private
-
1
def ns_shutdown_execution
-
@running.make_false
-
super
-
end
-
-
# @!visibility private
-
1
def ns_kill_execution
-
@running.make_false
-
super
-
end
-
-
# @!visibility private
-
1
def schedule_next_task(interval = execution_interval)
-
ScheduledTask.execute(interval, args: [Concurrent::Event.new], &method(:execute_task))
-
nil
-
end
-
-
# @!visibility private
-
1
def execute_task(completion)
-
return nil unless @running.true?
-
ScheduledTask.execute(timeout_interval, args: [completion], &method(:timeout_task))
-
_success, value, reason = @executor.execute(self)
-
if completion.try?
-
self.value = value
-
schedule_next_task
-
time = Time.now
-
observers.notify_observers do
-
[time, self.value, reason]
-
end
-
end
-
nil
-
end
-
-
# @!visibility private
-
1
def timeout_task(completion)
-
return unless @running.true?
-
if completion.try?
-
self.value = value
-
schedule_next_task
-
observers.notify_observers(Time.now, nil, Concurrent::TimeoutError.new)
-
end
-
end
-
end
-
end
-
1
require 'concurrent/atomic/atomic_reference'
-
-
1
module Concurrent
-
-
# A fixed size array with volatile (synchronized, thread safe) getters/setters.
-
# Mixes in Ruby's `Enumerable` module for enhanced search, sort, and traversal.
-
#
-
# @example
-
# tuple = Concurrent::Tuple.new(16)
-
#
-
# tuple.set(0, :foo) #=> :foo | volatile write
-
# tuple.get(0) #=> :foo | volatile read
-
# tuple.compare_and_set(0, :foo, :bar) #=> true | strong CAS
-
# tuple.cas(0, :foo, :baz) #=> false | strong CAS
-
# tuple.get(0) #=> :bar | volatile read
-
#
-
# @see https://en.wikipedia.org/wiki/Tuple Tuple entry at Wikipedia
-
# @see http://www.erlang.org/doc/reference_manual/data_types.html#id70396 Erlang Tuple
-
# @see http://ruby-doc.org/core-2.2.2/Enumerable.html Enumerable
-
1
class Tuple
-
1
include Enumerable
-
-
# The (fixed) size of the tuple.
-
1
attr_reader :size
-
-
# @!visibility private
-
1
Tuple = defined?(Rubinius::Tuple) ? Rubinius::Tuple : ::Array
-
1
private_constant :Tuple
-
-
# Create a new tuple of the given size.
-
#
-
# @param [Integer] size the number of elements in the tuple
-
1
def initialize(size)
-
@size = size
-
@tuple = tuple = Tuple.new(size)
-
i = 0
-
while i < size
-
tuple[i] = Concurrent::AtomicReference.new
-
i += 1
-
end
-
end
-
-
# Get the value of the element at the given index.
-
#
-
# @param [Integer] i the index from which to retrieve the value
-
# @return [Object] the value at the given index or nil if the index is out of bounds
-
1
def get(i)
-
return nil if i >= @size || i < 0
-
@tuple[i].get
-
end
-
1
alias_method :volatile_get, :get
-
-
# Set the element at the given index to the given value
-
#
-
# @param [Integer] i the index for the element to set
-
# @param [Object] value the value to set at the given index
-
#
-
# @return [Object] the new value of the element at the given index or nil if the index is out of bounds
-
1
def set(i, value)
-
return nil if i >= @size || i < 0
-
@tuple[i].set(value)
-
end
-
1
alias_method :volatile_set, :set
-
-
# Set the value at the given index to the new value if and only if the current
-
# value matches the given old value.
-
#
-
# @param [Integer] i the index for the element to set
-
# @param [Object] old_value the value to compare against the current value
-
# @param [Object] new_value the value to set at the given index
-
#
-
# @return [Boolean] true if the value at the given element was set else false
-
1
def compare_and_set(i, old_value, new_value)
-
return false if i >= @size || i < 0
-
@tuple[i].compare_and_set(old_value, new_value)
-
end
-
1
alias_method :cas, :compare_and_set
-
-
# Calls the given block once for each element in self, passing that element as a parameter.
-
#
-
# @yieldparam [Object] ref the `Concurrent::AtomicReference` object at the current index
-
1
def each
-
@tuple.each {|ref| yield ref.get}
-
end
-
end
-
end
-
1
require 'set'
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
# A `TVar` is a transactional variable - a single-element container that
-
# is used as part of a transaction - see `Concurrent::atomically`.
-
#
-
# @!macro thread_safe_variable_comparison
-
#
-
# {include:file:docs-source/tvar.md}
-
1
class TVar < Synchronization::Object
-
1
safe_initialization!
-
-
# Create a new `TVar` with an initial value.
-
1
def initialize(value)
-
@value = value
-
@version = 0
-
@lock = Mutex.new
-
end
-
-
# Get the value of a `TVar`.
-
1
def value
-
Concurrent::atomically do
-
Transaction::current.read(self)
-
end
-
end
-
-
# Set the value of a `TVar`.
-
1
def value=(value)
-
Concurrent::atomically do
-
Transaction::current.write(self, value)
-
end
-
end
-
-
# @!visibility private
-
1
def unsafe_value # :nodoc:
-
@value
-
end
-
-
# @!visibility private
-
1
def unsafe_value=(value) # :nodoc:
-
@value = value
-
end
-
-
# @!visibility private
-
1
def unsafe_version # :nodoc:
-
@version
-
end
-
-
# @!visibility private
-
1
def unsafe_increment_version # :nodoc:
-
@version += 1
-
end
-
-
# @!visibility private
-
1
def unsafe_lock # :nodoc:
-
@lock
-
end
-
-
end
-
-
# Run a block that reads and writes `TVar`s as a single atomic transaction.
-
# With respect to the value of `TVar` objects, the transaction is atomic, in
-
# that it either happens or it does not, consistent, in that the `TVar`
-
# objects involved will never enter an illegal state, and isolated, in that
-
# transactions never interfere with each other. You may recognise these
-
# properties from database transactions.
-
#
-
# There are some very important and unusual semantics that you must be aware of:
-
#
-
# * Most importantly, the block that you pass to atomically may be executed
-
# more than once. In most cases your code should be free of
-
# side-effects, except for via TVar.
-
#
-
# * If an exception escapes an atomically block it will abort the transaction.
-
#
-
# * It is undefined behaviour to use callcc or Fiber with atomically.
-
#
-
# * If you create a new thread within an atomically, it will not be part of
-
# the transaction. Creating a thread counts as a side-effect.
-
#
-
# Transactions within transactions are flattened to a single transaction.
-
#
-
# @example
-
# a = new TVar(100_000)
-
# b = new TVar(100)
-
#
-
# Concurrent::atomically do
-
# a.value -= 10
-
# b.value += 10
-
# end
-
1
def atomically
-
raise ArgumentError.new('no block given') unless block_given?
-
-
# Get the current transaction
-
-
transaction = Transaction::current
-
-
# Are we not already in a transaction (not nested)?
-
-
if transaction.nil?
-
# New transaction
-
-
begin
-
# Retry loop
-
-
loop do
-
-
# Create a new transaction
-
-
transaction = Transaction.new
-
Transaction::current = transaction
-
-
# Run the block, aborting on exceptions
-
-
begin
-
result = yield
-
rescue Transaction::AbortError => e
-
transaction.abort
-
result = Transaction::ABORTED
-
rescue Transaction::LeaveError => e
-
transaction.abort
-
break result
-
rescue => e
-
transaction.abort
-
raise e
-
end
-
# If we can commit, break out of the loop
-
-
if result != Transaction::ABORTED
-
if transaction.commit
-
break result
-
end
-
end
-
end
-
ensure
-
# Clear the current transaction
-
-
Transaction::current = nil
-
end
-
else
-
# Nested transaction - flatten it and just run the block
-
-
yield
-
end
-
end
-
-
# Abort a currently running transaction - see `Concurrent::atomically`.
-
1
def abort_transaction
-
raise Transaction::AbortError.new
-
end
-
-
# Leave a transaction without committing or aborting - see `Concurrent::atomically`.
-
1
def leave_transaction
-
raise Transaction::LeaveError.new
-
end
-
-
1
module_function :atomically, :abort_transaction, :leave_transaction
-
-
1
private
-
-
1
class Transaction
-
-
1
ABORTED = ::Object.new
-
-
1
ReadLogEntry = Struct.new(:tvar, :version)
-
-
1
AbortError = Class.new(StandardError)
-
1
LeaveError = Class.new(StandardError)
-
-
1
def initialize
-
@read_log = []
-
@write_log = {}
-
end
-
-
1
def read(tvar)
-
Concurrent::abort_transaction unless valid?
-
-
if @write_log.has_key? tvar
-
@write_log[tvar]
-
else
-
@read_log.push(ReadLogEntry.new(tvar, tvar.unsafe_version))
-
tvar.unsafe_value
-
end
-
end
-
-
1
def write(tvar, value)
-
# Have we already written to this TVar?
-
-
unless @write_log.has_key? tvar
-
# Try to lock the TVar
-
-
unless tvar.unsafe_lock.try_lock
-
# Someone else is writing to this TVar - abort
-
Concurrent::abort_transaction
-
end
-
-
# If we previously wrote to it, check the version hasn't changed
-
-
@read_log.each do |log_entry|
-
if log_entry.tvar == tvar and tvar.unsafe_version > log_entry.version
-
Concurrent::abort_transaction
-
end
-
end
-
end
-
-
# Record the value written
-
-
@write_log[tvar] = value
-
end
-
-
1
def abort
-
unlock
-
end
-
-
1
def commit
-
return false unless valid?
-
-
@write_log.each_pair do |tvar, value|
-
tvar.unsafe_value = value
-
tvar.unsafe_increment_version
-
end
-
-
unlock
-
-
true
-
end
-
-
1
def valid?
-
@read_log.each do |log_entry|
-
unless @write_log.has_key? log_entry.tvar
-
if log_entry.tvar.unsafe_version > log_entry.version
-
return false
-
end
-
end
-
end
-
-
true
-
end
-
-
1
def unlock
-
@write_log.each_key do |tvar|
-
tvar.unsafe_lock.unlock
-
end
-
end
-
-
1
def self.current
-
Thread.current[:current_tvar_transaction]
-
end
-
-
1
def self.current=(transaction)
-
Thread.current[:current_tvar_transaction] = transaction
-
end
-
-
end
-
-
end
-
1
module Concurrent
-
1
module Utility
-
-
# @!visibility private
-
1
module EngineDetector
-
1
def on_jruby?
-
20
ruby_engine == 'jruby'
-
end
-
-
1
def on_jruby_9000?
-
on_jruby? && ruby_version(JRUBY_VERSION, :>=, 9, 0, 0)
-
end
-
-
1
def on_cruby?
-
10
ruby_engine == 'ruby'
-
end
-
-
1
def on_rbx?
-
1
ruby_engine == 'rbx'
-
end
-
-
1
def on_truffleruby?
-
2
ruby_engine == 'truffleruby'
-
end
-
-
1
def on_windows?
-
!(RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/).nil?
-
end
-
-
1
def on_osx?
-
!(RbConfig::CONFIG['host_os'] =~ /darwin|mac os/).nil?
-
end
-
-
1
def on_linux?
-
!(RbConfig::CONFIG['host_os'] =~ /linux/).nil?
-
end
-
-
1
def ruby_engine
-
33
defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'
-
end
-
-
1
def ruby_version(version = RUBY_VERSION, comparison, major, minor, patch)
-
2
result = (version.split('.').map(&:to_i) <=> [major, minor, patch])
-
2
comparisons = { :== => [0],
-
:>= => [1, 0],
-
:<= => [-1, 0],
-
:> => [1],
-
:< => [-1] }
-
2
comparisons.fetch(comparison).include? result
-
end
-
end
-
end
-
-
# @!visibility private
-
1
extend Utility::EngineDetector
-
end
-
1
require 'concurrent/synchronization'
-
-
1
module Concurrent
-
-
1
class_definition = Class.new(Synchronization::LockableObject) do
-
1
def initialize
-
1
@last_time = Time.now.to_f
-
1
super()
-
end
-
-
1
if defined?(Process::CLOCK_MONOTONIC)
-
# @!visibility private
-
1
def get_time
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
-
end
-
elsif Concurrent.on_jruby?
-
# @!visibility private
-
def get_time
-
java.lang.System.nanoTime() / 1_000_000_000.0
-
end
-
else
-
-
# @!visibility private
-
def get_time
-
synchronize do
-
now = Time.now.to_f
-
if @last_time < now
-
@last_time = now
-
else # clock has moved back in time
-
@last_time += 0.000_001
-
end
-
end
-
end
-
-
end
-
end
-
-
# Clock that cannot be set and represents monotonic time since
-
# some unspecified starting point.
-
#
-
# @!visibility private
-
1
GLOBAL_MONOTONIC_CLOCK = class_definition.new
-
1
private_constant :GLOBAL_MONOTONIC_CLOCK
-
-
# @!macro monotonic_get_time
-
#
-
# Returns the current time a tracked by the application monotonic clock.
-
#
-
# @return [Float] The current monotonic time since some unspecified
-
# starting point
-
#
-
# @!macro monotonic_clock_warning
-
1
def monotonic_time
-
GLOBAL_MONOTONIC_CLOCK.get_time
-
end
-
-
1
module_function :monotonic_time
-
end
-
1
require 'concurrent/utility/engine'
-
-
1
module Concurrent
-
-
1
module Utility
-
-
# @!visibility private
-
1
module NativeExtensionLoader
-
-
1
def allow_c_extensions?
-
Concurrent.on_cruby?
-
end
-
-
1
def c_extensions_loaded?
-
2
defined?(@c_extensions_loaded) && @c_extensions_loaded
-
end
-
-
1
def java_extensions_loaded?
-
defined?(@java_extensions_loaded) && @java_extensions_loaded
-
end
-
-
1
def load_native_extensions
-
1
unless defined? Synchronization::AbstractObject
-
raise 'native_extension_loader loaded before Synchronization::AbstractObject'
-
end
-
-
1
if Concurrent.on_cruby? && !c_extensions_loaded?
-
1
['concurrent/concurrent_ruby_ext',
-
"concurrent/#{RUBY_VERSION[0..2]}/concurrent_ruby_ext"
-
2
].each { |p| try_load_c_extension p }
-
end
-
-
1
if Concurrent.on_jruby? && !java_extensions_loaded?
-
begin
-
require 'concurrent/concurrent_ruby.jar'
-
set_java_extensions_loaded
-
rescue LoadError => e
-
raise e, "Java extensions are required for JRuby.\n" + e.message, e.backtrace
-
end
-
end
-
end
-
-
1
private
-
-
1
def load_error_path(error)
-
2
if error.respond_to? :path
-
2
error.path
-
else
-
error.message.split(' -- ').last
-
end
-
end
-
-
1
def set_c_extensions_loaded
-
@c_extensions_loaded = true
-
end
-
-
1
def set_java_extensions_loaded
-
@java_extensions_loaded = true
-
end
-
-
1
def try_load_c_extension(path)
-
2
require path
-
set_c_extensions_loaded
-
rescue LoadError => e
-
2
if load_error_path(e) == path
-
# move on with pure-Ruby implementations
-
# TODO (pitr-ch 12-Jul-2018): warning on verbose?
-
else
-
raise e
-
end
-
end
-
-
end
-
end
-
-
# @!visibility private
-
1
extend Utility::NativeExtensionLoader
-
end
-
-
1
module Concurrent
-
1
module Utility
-
# @private
-
1
module NativeInteger
-
# http://stackoverflow.com/questions/535721/ruby-max-integer
-
1
MIN_VALUE = -(2**(0.size * 8 - 2))
-
1
MAX_VALUE = (2**(0.size * 8 - 2) - 1)
-
-
1
def ensure_upper_bound(value)
-
if value > MAX_VALUE
-
raise RangeError.new("#{value} is greater than the maximum value of #{MAX_VALUE}")
-
end
-
value
-
end
-
-
1
def ensure_lower_bound(value)
-
if value < MIN_VALUE
-
raise RangeError.new("#{value} is less than the maximum value of #{MIN_VALUE}")
-
end
-
value
-
end
-
-
1
def ensure_integer(value)
-
unless value.is_a?(Integer)
-
raise ArgumentError.new("#{value} is not an Integer")
-
end
-
value
-
end
-
-
1
def ensure_integer_and_bounds(value)
-
ensure_integer value
-
ensure_upper_bound value
-
ensure_lower_bound value
-
end
-
-
1
def ensure_positive(value)
-
if value < 0
-
raise ArgumentError.new("#{value} cannot be negative")
-
end
-
value
-
end
-
-
1
def ensure_positive_and_no_zero(value)
-
if value < 1
-
raise ArgumentError.new("#{value} cannot be negative or zero")
-
end
-
value
-
end
-
-
1
extend self
-
end
-
end
-
end
-
1
require 'etc'
-
1
require 'rbconfig'
-
1
require 'concurrent/delay'
-
-
1
module Concurrent
-
1
module Utility
-
-
# @!visibility private
-
1
class ProcessorCounter
-
1
def initialize
-
1
@processor_count = Delay.new { compute_processor_count }
-
1
@physical_processor_count = Delay.new { compute_physical_processor_count }
-
end
-
-
# Number of processors seen by the OS and used for process scheduling. For
-
# performance reasons the calculated value will be memoized on the first
-
# call.
-
#
-
# When running under JRuby the Java runtime call
-
# `java.lang.Runtime.getRuntime.availableProcessors` will be used. According
-
# to the Java documentation this "value may change during a particular
-
# invocation of the virtual machine... [applications] should therefore
-
# occasionally poll this property." Subsequently the result will NOT be
-
# memoized under JRuby.
-
#
-
# Ruby's Etc.nprocessors will be used if available (MRI 2.2+).
-
#
-
# On Windows the Win32 API will be queried for the
-
# `NumberOfLogicalProcessors from Win32_Processor`. This will return the
-
# total number "logical processors for the current instance of the
-
# processor", which taked into account hyperthreading.
-
#
-
# * AIX: /usr/sbin/pmcycles (AIX 5+), /usr/sbin/lsdev
-
# * Alpha: /usr/bin/nproc (/proc/cpuinfo exists but cannot be used)
-
# * BSD: /sbin/sysctl
-
# * Cygwin: /proc/cpuinfo
-
# * Darwin: /usr/bin/hwprefs, /usr/sbin/sysctl
-
# * HP-UX: /usr/sbin/ioscan
-
# * IRIX: /usr/sbin/sysconf
-
# * Linux: /proc/cpuinfo
-
# * Minix 3+: /proc/cpuinfo
-
# * Solaris: /usr/sbin/psrinfo
-
# * Tru64 UNIX: /usr/sbin/psrinfo
-
# * UnixWare: /usr/sbin/psrinfo
-
#
-
# @return [Integer] number of processors seen by the OS or Java runtime
-
#
-
# @see https://github.com/grosser/parallel/blob/4fc8b89d08c7091fe0419ca8fba1ec3ce5a8d185/lib/parallel.rb
-
#
-
# @see http://docs.oracle.com/javase/6/docs/api/java/lang/Runtime.html#availableProcessors()
-
# @see http://msdn.microsoft.com/en-us/library/aa394373(v=vs.85).aspx
-
1
def processor_count
-
@processor_count.value
-
end
-
-
# Number of physical processor cores on the current system. For performance
-
# reasons the calculated value will be memoized on the first call.
-
#
-
# On Windows the Win32 API will be queried for the `NumberOfCores from
-
# Win32_Processor`. This will return the total number "of cores for the
-
# current instance of the processor." On Unix-like operating systems either
-
# the `hwprefs` or `sysctl` utility will be called in a subshell and the
-
# returned value will be used. In the rare case where none of these methods
-
# work or an exception is raised the function will simply return 1.
-
#
-
# @return [Integer] number physical processor cores on the current system
-
#
-
# @see https://github.com/grosser/parallel/blob/4fc8b89d08c7091fe0419ca8fba1ec3ce5a8d185/lib/parallel.rb
-
#
-
# @see http://msdn.microsoft.com/en-us/library/aa394373(v=vs.85).aspx
-
# @see http://www.unix.com/man-page/osx/1/HWPREFS/
-
# @see http://linux.die.net/man/8/sysctl
-
1
def physical_processor_count
-
@physical_processor_count.value
-
end
-
-
1
private
-
-
1
def compute_processor_count
-
if Concurrent.on_jruby?
-
java.lang.Runtime.getRuntime.availableProcessors
-
elsif Etc.respond_to?(:nprocessors) && (nprocessor = Etc.nprocessors rescue nil)
-
nprocessor
-
else
-
os_name = RbConfig::CONFIG["target_os"]
-
if os_name =~ /mingw|mswin/
-
require 'win32ole'
-
result = WIN32OLE.connect("winmgmts://").ExecQuery(
-
"select NumberOfLogicalProcessors from Win32_Processor")
-
result.to_enum.collect(&:NumberOfLogicalProcessors).reduce(:+)
-
elsif File.readable?("/proc/cpuinfo") && (cpuinfo_count = IO.read("/proc/cpuinfo").scan(/^processor/).size) > 0
-
cpuinfo_count
-
elsif File.executable?("/usr/bin/nproc")
-
IO.popen("/usr/bin/nproc --all", &:read).to_i
-
elsif File.executable?("/usr/bin/hwprefs")
-
IO.popen("/usr/bin/hwprefs thread_count", &:read).to_i
-
elsif File.executable?("/usr/sbin/psrinfo")
-
IO.popen("/usr/sbin/psrinfo", &:read).scan(/^.*on-*line/).size
-
elsif File.executable?("/usr/sbin/ioscan")
-
IO.popen("/usr/sbin/ioscan -kC processor", &:read).scan(/^.*processor/).size
-
elsif File.executable?("/usr/sbin/pmcycles")
-
IO.popen("/usr/sbin/pmcycles -m", &:read).count("\n")
-
elsif File.executable?("/usr/sbin/lsdev")
-
IO.popen("/usr/sbin/lsdev -Cc processor -S 1", &:read).count("\n")
-
elsif File.executable?("/usr/sbin/sysconf") and os_name =~ /irix/i
-
IO.popen("/usr/sbin/sysconf NPROC_ONLN", &:read).to_i
-
elsif File.executable?("/usr/sbin/sysctl")
-
IO.popen("/usr/sbin/sysctl -n hw.ncpu", &:read).to_i
-
elsif File.executable?("/sbin/sysctl")
-
IO.popen("/sbin/sysctl -n hw.ncpu", &:read).to_i
-
else
-
# TODO (pitr-ch 05-Nov-2016): warn about failures
-
1
-
end
-
end
-
rescue
-
return 1
-
end
-
-
1
def compute_physical_processor_count
-
ppc = case RbConfig::CONFIG["target_os"]
-
when /darwin1/
-
IO.popen("/usr/sbin/sysctl -n hw.physicalcpu", &:read).to_i
-
when /linux/
-
cores = {} # unique physical ID / core ID combinations
-
phy = 0
-
IO.read("/proc/cpuinfo").scan(/^physical id.*|^core id.*/) do |ln|
-
if ln.start_with?("physical")
-
phy = ln[/\d+/]
-
elsif ln.start_with?("core")
-
cid = phy + ":" + ln[/\d+/]
-
cores[cid] = true if not cores[cid]
-
end
-
end
-
cores.count
-
when /mswin|mingw/
-
require 'win32ole'
-
result_set = WIN32OLE.connect("winmgmts://").ExecQuery(
-
"select NumberOfCores from Win32_Processor")
-
result_set.to_enum.collect(&:NumberOfCores).reduce(:+)
-
else
-
processor_count
-
end
-
# fall back to logical count if physical info is invalid
-
ppc > 0 ? ppc : processor_count
-
rescue
-
return 1
-
end
-
end
-
end
-
-
# create the default ProcessorCounter on load
-
1
@processor_counter = Utility::ProcessorCounter.new
-
1
singleton_class.send :attr_reader, :processor_counter
-
-
1
def self.processor_count
-
processor_counter.processor_count
-
end
-
-
1
def self.physical_processor_count
-
processor_counter.physical_processor_count
-
end
-
end
-
1
module Concurrent
-
1
VERSION = '1.1.6'
-
end
-
1
require "execjs/module"
-
1
require "execjs/runtimes"
-
-
1
module ExecJS
-
1
self.runtime ||= Runtimes.autodetect
-
end
-
1
require "execjs/runtime"
-
-
1
module ExecJS
-
1
class DisabledRuntime < Runtime
-
1
def name
-
"Disabled"
-
end
-
-
1
def exec(source, options = {})
-
raise Error, "ExecJS disabled"
-
end
-
-
1
def eval(source, options = {})
-
raise Error, "ExecJS disabled"
-
end
-
-
1
def compile(source, options = {})
-
raise Error, "ExecJS disabled"
-
end
-
-
1
def deprecated?
-
true
-
end
-
-
1
def available?
-
true
-
end
-
end
-
end
-
1
require "execjs/runtime"
-
1
require "json"
-
-
1
module ExecJS
-
1
class DuktapeRuntime < Runtime
-
1
class Context < Runtime::Context
-
1
def initialize(runtime, source = "", options = {})
-
@ctx = Duktape::Context.new(complex_object: nil)
-
@ctx.exec_string(encode(source), '(execjs)')
-
rescue Exception => e
-
raise wrap_error(e)
-
end
-
-
1
def exec(source, options = {})
-
return unless /\S/ =~ source
-
@ctx.eval_string("(function(){#{encode(source)}})()", '(execjs)')
-
rescue Exception => e
-
raise wrap_error(e)
-
end
-
-
1
def eval(source, options = {})
-
return unless /\S/ =~ source
-
@ctx.eval_string("(#{encode(source)})", '(execjs)')
-
rescue Exception => e
-
raise wrap_error(e)
-
end
-
-
1
def call(identifier, *args)
-
@ctx.call_prop(identifier.split("."), *args)
-
rescue Exception => e
-
raise wrap_error(e)
-
end
-
-
1
private
-
1
def wrap_error(e)
-
klass = case e
-
when Duktape::SyntaxError
-
RuntimeError
-
when Duktape::Error
-
ProgramError
-
when Duktape::InternalError
-
RuntimeError
-
end
-
-
if klass
-
re = / \(line (\d+)\)$/
-
lineno = e.message[re, 1] || 1
-
error = klass.new(e.message.sub(re, ""))
-
error.set_backtrace(["(execjs):#{lineno}"] + e.backtrace)
-
error
-
else
-
e
-
end
-
end
-
end
-
-
1
def name
-
"Duktape"
-
end
-
-
1
def available?
-
1
require "duktape"
-
true
-
rescue LoadError
-
1
false
-
end
-
end
-
end
-
1
module ExecJS
-
# Encodes strings as UTF-8
-
1
module Encoding
-
1
if RUBY_ENGINE == 'jruby' || RUBY_ENGINE == 'rbx'
-
# workaround for jruby bug http://jira.codehaus.org/browse/JRUBY-6588
-
# workaround for rbx bug https://github.com/rubinius/rubinius/issues/1729
-
def encode(string)
-
if string.encoding.name == 'ASCII-8BIT'
-
data = string.dup
-
data.force_encoding('UTF-8')
-
-
unless data.valid_encoding?
-
raise ::Encoding::UndefinedConversionError, "Could not encode ASCII-8BIT data #{string.dump} as UTF-8"
-
end
-
else
-
data = string.encode('UTF-8')
-
end
-
data
-
end
-
else
-
1
def encode(string)
-
4
string.encode('UTF-8')
-
end
-
end
-
end
-
end
-
1
require "tmpdir"
-
1
require "execjs/runtime"
-
-
1
module ExecJS
-
1
class ExternalRuntime < Runtime
-
1
class Context < Runtime::Context
-
1
def initialize(runtime, source = "", options = {})
-
1
source = encode(source)
-
-
1
@runtime = runtime
-
1
@source = source
-
-
# Test compile context source
-
1
exec("")
-
end
-
-
1
def eval(source, options = {})
-
1
source = encode(source)
-
-
1
if /\S/ =~ source
-
1
exec("return eval(#{::JSON.generate("(#{source})", quirks_mode: true)})")
-
end
-
end
-
-
1
def exec(source, options = {})
-
2
source = encode(source)
-
2
source = "#{@source}\n#{source}" if @source != ""
-
2
source = @runtime.compile_source(source)
-
-
2
tmpfile = write_to_tempfile(source)
-
-
2
if ExecJS.cygwin?
-
filepath = `cygpath -m #{tmpfile.path}`.rstrip
-
else
-
2
filepath = tmpfile.path
-
end
-
-
begin
-
2
extract_result(@runtime.exec_runtime(filepath), filepath)
-
ensure
-
2
File.unlink(tmpfile)
-
end
-
end
-
-
1
def call(identifier, *args)
-
1
eval "#{identifier}.apply(this, #{::JSON.generate(args)})"
-
end
-
-
1
protected
-
# See Tempfile.create on Ruby 2.1
-
1
def create_tempfile(basename)
-
2
tmpfile = nil
-
2
Dir::Tmpname.create(basename) do |tmpname|
-
2
mode = File::WRONLY | File::CREAT | File::EXCL
-
2
tmpfile = File.open(tmpname, mode, 0600)
-
end
-
2
tmpfile
-
end
-
-
1
def write_to_tempfile(contents)
-
2
tmpfile = create_tempfile(['execjs', 'js'])
-
2
tmpfile.write(contents)
-
2
tmpfile.close
-
2
tmpfile
-
end
-
-
1
def extract_result(output, filename)
-
2
status, value, stack = output.empty? ? [] : ::JSON.parse(output, create_additions: false)
-
2
if status == "ok"
-
2
value
-
else
-
stack ||= ""
-
real_filename = File.realpath(filename)
-
stack = stack.split("\n").map do |line|
-
line.sub(" at ", "")
-
.sub(real_filename, "(execjs)")
-
.sub(filename, "(execjs)")
-
.strip
-
end
-
stack.reject! { |line| ["eval code", "eval@[native code]"].include?(line) }
-
stack.shift unless stack[0].to_s.include?("(execjs)")
-
error_class = value =~ /SyntaxError:/ ? RuntimeError : ProgramError
-
error = error_class.new(value)
-
error.set_backtrace(stack + caller)
-
raise error
-
end
-
end
-
end
-
-
1
attr_reader :name
-
-
1
def initialize(options)
-
5
@name = options[:name]
-
5
@command = options[:command]
-
5
@runner_path = options[:runner_path]
-
5
@encoding = options[:encoding]
-
5
@deprecated = !!options[:deprecated]
-
5
@binary = nil
-
-
5
@popen_options = {}
-
5
@popen_options[:external_encoding] = @encoding if @encoding
-
5
@popen_options[:internal_encoding] = ::Encoding.default_internal || 'UTF-8'
-
-
5
if @runner_path
-
5
instance_eval generate_compile_method(@runner_path)
-
end
-
end
-
-
1
def available?
-
2
require 'json'
-
2
binary ? true : false
-
end
-
-
1
def deprecated?
-
5
@deprecated
-
end
-
-
1
private
-
1
def binary
-
4
@binary ||= which(@command)
-
end
-
-
1
def locate_executable(command)
-
2
commands = Array(command)
-
2
if ExecJS.windows? && File.extname(command) == ""
-
ENV['PATHEXT'].split(File::PATH_SEPARATOR).each { |p|
-
commands << (command + p)
-
}
-
end
-
-
2
commands.find { |cmd|
-
2
if File.executable? cmd
-
cmd
-
else
-
2
path = ENV['PATH'].split(File::PATH_SEPARATOR).find { |p|
-
12
full_path = File.join(p, cmd)
-
12
File.executable?(full_path) && File.file?(full_path)
-
}
-
2
path && File.expand_path(cmd, path)
-
end
-
}
-
end
-
-
1
protected
-
1
def generate_compile_method(path)
-
5
<<-RUBY
-
def compile_source(source)
-
<<-RUNNER
-
#{IO.read(path)}
-
RUNNER
-
end
-
RUBY
-
end
-
-
1
def json2_source
-
@json2_source ||= IO.read(ExecJS.root + "/support/json2.js")
-
end
-
-
1
def encode_source(source)
-
encoded_source = encode_unicode_codepoints(source)
-
::JSON.generate("(function(){ #{encoded_source} })()", quirks_mode: true)
-
end
-
-
1
def encode_unicode_codepoints(str)
-
str.gsub(/[\u0080-\uffff]/) do |ch|
-
"\\u%04x" % ch.codepoints.to_a
-
end
-
end
-
-
1
if ExecJS.windows?
-
def exec_runtime(filename)
-
path = Dir::Tmpname.create(['execjs', 'json']) {}
-
begin
-
command = binary.split(" ") << filename
-
`#{shell_escape(*command)} 2>&1 > #{path}`
-
output = File.open(path, 'rb', @popen_options) { |f| f.read }
-
ensure
-
File.unlink(path) if path
-
end
-
-
if $?.success?
-
output
-
else
-
raise exec_runtime_error(output)
-
end
-
end
-
-
def shell_escape(*args)
-
# see http://technet.microsoft.com/en-us/library/cc723564.aspx#XSLTsection123121120120
-
args.map { |arg|
-
arg = %Q("#{arg.gsub('"','""')}") if arg.match(/[&|()<>^ "]/)
-
arg
-
}.join(" ")
-
end
-
1
elsif RUBY_ENGINE == 'jruby'
-
require 'shellwords'
-
-
def exec_runtime(filename)
-
command = "#{Shellwords.join(binary.split(' ') << filename)} 2>&1"
-
io = IO.popen(command, @popen_options)
-
output = io.read
-
io.close
-
-
if $?.success?
-
output
-
else
-
raise exec_runtime_error(output)
-
end
-
end
-
else
-
1
def exec_runtime(filename)
-
2
io = IO.popen(binary.split(' ') << filename, @popen_options.merge({err: [:child, :out]}))
-
2
output = io.read
-
2
io.close
-
-
2
if $?.success?
-
2
output
-
else
-
raise exec_runtime_error(output)
-
end
-
end
-
end
-
# Internally exposed for Context.
-
1
public :exec_runtime
-
-
1
def exec_runtime_error(output)
-
error = RuntimeError.new(output)
-
lines = output.split("\n")
-
lineno = lines[0][/:(\d+)$/, 1] if lines[0]
-
lineno ||= 1
-
error.set_backtrace(["(execjs):#{lineno}"] + caller)
-
error
-
end
-
-
1
def which(command)
-
1
Array(command).find do |name|
-
2
name, args = name.split(/\s+/, 2)
-
2
path = locate_executable(name)
-
-
2
next unless path
-
-
1
args ? "#{path} #{args}" : path
-
end
-
end
-
end
-
end
-
1
require "execjs/version"
-
1
require "rbconfig"
-
-
1
module ExecJS
-
1
class Error < ::StandardError; end
-
1
class RuntimeError < Error; end
-
1
class ProgramError < Error; end
-
1
class RuntimeUnavailable < RuntimeError; end
-
-
1
class << self
-
1
attr_reader :runtime
-
-
1
def runtime=(runtime)
-
1
raise RuntimeUnavailable, "#{runtime.name} is unavailable on this system" unless runtime.available?
-
1
@runtime = runtime
-
end
-
-
1
def exec(source, options = {})
-
runtime.exec(source, options)
-
end
-
-
1
def eval(source, options = {})
-
runtime.eval(source, options)
-
end
-
-
1
def compile(source, options = {})
-
1
runtime.compile(source, options)
-
end
-
-
1
def root
-
5
@root ||= File.expand_path("..", __FILE__)
-
end
-
-
1
def windows?
-
3
@windows ||= RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
-
end
-
-
1
def cygwin?
-
2
@cygwin ||= RbConfig::CONFIG["host_os"] =~ /cygwin/
-
end
-
end
-
end
-
1
require "execjs/runtime"
-
-
1
module ExecJS
-
1
class RubyRacerRuntime < Runtime
-
1
class Context < Runtime::Context
-
1
def initialize(runtime, source = "", options = {})
-
source = encode(source)
-
-
lock do
-
@v8_context = ::V8::Context.new
-
-
begin
-
@v8_context.eval(source)
-
rescue ::V8::JSError => e
-
raise wrap_error(e)
-
end
-
end
-
end
-
-
1
def exec(source, options = {})
-
source = encode(source)
-
-
if /\S/ =~ source
-
eval "(function(){#{source}})()", options
-
end
-
end
-
-
1
def eval(source, options = {})
-
source = encode(source)
-
-
if /\S/ =~ source
-
lock do
-
begin
-
unbox @v8_context.eval("(#{source})")
-
rescue ::V8::JSError => e
-
raise wrap_error(e)
-
end
-
end
-
end
-
end
-
-
1
def call(properties, *args)
-
lock do
-
begin
-
unbox @v8_context.eval(properties).call(*args)
-
rescue ::V8::JSError => e
-
raise wrap_error(e)
-
end
-
end
-
end
-
-
1
def unbox(value)
-
case value
-
when ::V8::Function
-
nil
-
when ::V8::Array
-
value.map { |v| unbox(v) }
-
when ::V8::Object
-
value.inject({}) do |vs, (k, v)|
-
vs[k] = unbox(v) unless v.is_a?(::V8::Function)
-
vs
-
end
-
when String
-
value.force_encoding('UTF-8')
-
else
-
value
-
end
-
end
-
-
1
private
-
1
def lock
-
result, exception = nil, nil
-
V8::C::Locker() do
-
begin
-
result = yield
-
rescue Exception => e
-
exception = e
-
end
-
end
-
-
if exception
-
raise exception
-
else
-
result
-
end
-
end
-
-
1
def wrap_error(e)
-
error_class = e.value["name"] == "SyntaxError" ? RuntimeError : ProgramError
-
-
stack = e.value["stack"] || ""
-
stack = stack.split("\n")
-
stack.shift
-
stack = [e.message[/<eval>:\d+:\d+/, 0]].compact if stack.empty?
-
stack = stack.map { |line| line.sub(" at ", "").sub("<eval>", "(execjs)").strip }
-
-
error = error_class.new(e.value.to_s)
-
error.set_backtrace(stack + caller)
-
error
-
end
-
end
-
-
1
def name
-
"therubyracer (V8)"
-
end
-
-
1
def available?
-
1
require "v8"
-
true
-
rescue LoadError
-
1
false
-
end
-
end
-
end
-
1
require "execjs/runtime"
-
-
1
module ExecJS
-
1
class RubyRhinoRuntime < Runtime
-
1
class Context < Runtime::Context
-
1
def initialize(runtime, source = "", options = {})
-
source = encode(source)
-
-
@rhino_context = ::Rhino::Context.new
-
fix_memory_limit! @rhino_context
-
@rhino_context.eval(source)
-
rescue Exception => e
-
raise wrap_error(e)
-
end
-
-
1
def exec(source, options = {})
-
source = encode(source)
-
-
if /\S/ =~ source
-
eval "(function(){#{source}})()", options
-
end
-
end
-
-
1
def eval(source, options = {})
-
source = encode(source)
-
-
if /\S/ =~ source
-
unbox @rhino_context.eval("(#{source})")
-
end
-
rescue Exception => e
-
raise wrap_error(e)
-
end
-
-
1
def call(properties, *args)
-
unbox @rhino_context.eval(properties).call(*args)
-
rescue Exception => e
-
raise wrap_error(e)
-
end
-
-
1
def unbox(value)
-
case value = ::Rhino::to_ruby(value)
-
when Java::OrgMozillaJavascript::NativeFunction
-
nil
-
when Java::OrgMozillaJavascript::NativeObject
-
value.inject({}) do |vs, (k, v)|
-
case v
-
when Java::OrgMozillaJavascript::NativeFunction, ::Rhino::JS::Function
-
nil
-
else
-
vs[k] = unbox(v)
-
end
-
vs
-
end
-
when Array
-
value.map { |v| unbox(v) }
-
else
-
value
-
end
-
end
-
-
1
def wrap_error(e)
-
return e unless e.is_a?(::Rhino::JSError)
-
-
error_class = e.message == "syntax error" ? RuntimeError : ProgramError
-
-
stack = e.backtrace
-
stack = stack.map { |line| line.sub(" at ", "").sub("<eval>", "(execjs)").strip }
-
stack.unshift("(execjs):1") if e.javascript_backtrace.empty?
-
-
error = error_class.new(e.value.to_s)
-
error.set_backtrace(stack)
-
error
-
end
-
-
1
private
-
# Disables bytecode compiling which limits you to 64K scripts
-
1
def fix_memory_limit!(context)
-
if context.respond_to?(:optimization_level=)
-
context.optimization_level = -1
-
else
-
context.instance_eval { @native.setOptimizationLevel(-1) }
-
end
-
end
-
end
-
-
1
def name
-
"therubyrhino (Rhino)"
-
end
-
-
1
def available?
-
1
require "rhino"
-
true
-
rescue LoadError
-
1
false
-
end
-
end
-
end
-
1
require "execjs/encoding"
-
-
1
module ExecJS
-
# Abstract base class for runtimes
-
1
class Runtime
-
1
class Context
-
1
include Encoding
-
-
1
def initialize(runtime, source = "", options = {})
-
end
-
-
1
def exec(source, options = {})
-
raise NotImplementedError
-
end
-
-
1
def eval(source, options = {})
-
raise NotImplementedError
-
end
-
-
1
def call(properties, *args)
-
raise NotImplementedError
-
end
-
end
-
-
1
def name
-
raise NotImplementedError
-
end
-
-
1
def context_class
-
2
self.class::Context
-
end
-
-
1
def exec(source, options = {})
-
context = compile("", options)
-
-
if context.method(:exec).arity == 1
-
context.exec(source)
-
else
-
context.exec(source, options)
-
end
-
end
-
-
1
def eval(source, options = {})
-
context = compile("", options)
-
-
if context.method(:eval).arity == 1
-
context.eval(source)
-
else
-
context.eval(source, options)
-
end
-
end
-
-
1
def compile(source, options = {})
-
1
if context_class.instance_method(:initialize).arity == 2
-
context_class.new(self, source)
-
else
-
1
context_class.new(self, source, options)
-
end
-
end
-
-
1
def deprecated?
-
4
false
-
end
-
-
1
def available?
-
raise NotImplementedError
-
end
-
end
-
end
-
1
require "execjs/module"
-
1
require "execjs/disabled_runtime"
-
1
require "execjs/duktape_runtime"
-
1
require "execjs/external_runtime"
-
1
require "execjs/ruby_racer_runtime"
-
1
require "execjs/ruby_rhino_runtime"
-
1
require "execjs/mini_racer_runtime"
-
-
1
module ExecJS
-
1
module Runtimes
-
1
Disabled = DisabledRuntime.new
-
-
1
Duktape = DuktapeRuntime.new
-
-
1
RubyRacer = RubyRacerRuntime.new
-
-
1
RubyRhino = RubyRhinoRuntime.new
-
-
1
MiniRacer = MiniRacerRuntime.new
-
-
1
Node = ExternalRuntime.new(
-
name: "Node.js (V8)",
-
command: ["nodejs", "node"],
-
runner_path: ExecJS.root + "/support/node_runner.js",
-
encoding: 'UTF-8'
-
)
-
-
1
JavaScriptCore = ExternalRuntime.new(
-
name: "JavaScriptCore",
-
command: "/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc",
-
runner_path: ExecJS.root + "/support/jsc_runner.js"
-
)
-
-
1
SpiderMonkey = Spidermonkey = ExternalRuntime.new(
-
name: "SpiderMonkey",
-
command: "js",
-
runner_path: ExecJS.root + "/support/spidermonkey_runner.js",
-
deprecated: true
-
)
-
-
1
JScript = ExternalRuntime.new(
-
name: "JScript",
-
command: "cscript //E:jscript //Nologo //U",
-
runner_path: ExecJS.root + "/support/jscript_runner.js",
-
encoding: 'UTF-16LE' # CScript with //U returns UTF-16LE
-
)
-
-
1
V8 = ExternalRuntime.new(
-
name: "V8",
-
command: "d8",
-
runner_path: ExecJS.root + "/support/v8_runner.js",
-
encoding: 'UTF-8'
-
)
-
-
-
1
def self.autodetect
-
1
from_environment || best_available ||
-
raise(RuntimeUnavailable, "Could not find a JavaScript runtime. " +
-
"See https://github.com/rails/execjs for a list of available runtimes.")
-
end
-
-
1
def self.best_available
-
1
runtimes.reject(&:deprecated?).find(&:available?)
-
end
-
-
1
def self.from_environment
-
1
if name = ENV["EXECJS_RUNTIME"]
-
raise RuntimeUnavailable, "#{name} runtime is not defined" unless const_defined?(name)
-
runtime = const_get(name)
-
-
raise RuntimeUnavailable, "#{runtime.name} runtime is not available on this system" unless runtime.available?
-
runtime
-
end
-
end
-
-
1
def self.names
-
@names ||= constants.inject({}) { |h, name| h.merge(const_get(name) => name) }.values
-
end
-
-
1
def self.runtimes
-
1
@runtimes ||= [
-
RubyRacer,
-
RubyRhino,
-
Duktape,
-
MiniRacer,
-
Node,
-
JavaScriptCore,
-
SpiderMonkey,
-
JScript,
-
V8
-
]
-
end
-
end
-
-
1
def self.runtimes
-
Runtimes.runtimes
-
end
-
end
-
1
module ExecJS
-
1
VERSION = "2.7.0"
-
end
-
1
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'rbx'
-
1
Object.send(:remove_const, :FFI) if defined?(::FFI)
-
begin
-
1
require RUBY_VERSION.split('.')[0, 2].join('.') + '/ffi_c'
-
rescue Exception
-
1
require 'ffi_c'
-
end
-
-
1
require 'ffi/ffi'
-
-
elsif RUBY_ENGINE == 'jruby' && Gem::Version.new(RUBY_ENGINE_VERSION) >= Gem::Version.new("9.3.pre")
-
JRuby::Util.load_ext("org.jruby.ext.ffi.FFIService")
-
require 'ffi/ffi'
-
-
elsif RUBY_ENGINE == 'truffleruby' && Gem::Version.new(RUBY_ENGINE_VERSION) >= Gem::Version.new("20.1.0-dev-a")
-
require 'truffleruby/ffi_backend'
-
require 'ffi/ffi'
-
-
else
-
# Remove the ffi gem dir from the load path, then reload the internal ffi implementation
-
$LOAD_PATH.delete(File.dirname(__FILE__))
-
$LOAD_PATH.delete(File.join(File.dirname(__FILE__), 'ffi'))
-
unless $LOADED_FEATURES.nil?
-
$LOADED_FEATURES.delete(__FILE__)
-
$LOADED_FEATURES.delete('ffi.rb')
-
end
-
require 'ffi.rb'
-
end
-
#
-
# Copyright (C) 2008-2010 Wayne Meissner
-
# Copyright (C) 2008 Mike Dalessio
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
1
module FFI
-
1
class AutoPointer < Pointer
-
1
extend DataConverter
-
-
# @overload initialize(pointer, method)
-
# @param pointer [Pointer]
-
# @param method [Method]
-
# @return [self]
-
# The passed Method will be invoked at GC time.
-
# @overload initialize(pointer, proc)
-
# @param pointer [Pointer]
-
# @return [self]
-
# The passed Proc will be invoked at GC time (SEE WARNING BELOW!)
-
# @note WARNING: passing a proc _may_ cause your pointer to never be
-
# GC'd, unless you're careful to avoid trapping a reference to the
-
# pointer in the proc. See the test specs for examples.
-
# @overload initialize(pointer) { |p| ... }
-
# @param pointer [Pointer]
-
# @yieldparam [Pointer] p +pointer+ passed to the block
-
# @return [self]
-
# The passed block will be invoked at GC time.
-
# @note
-
# WARNING: passing a block will cause your pointer to never be GC'd.
-
# This is bad.
-
# @overload initialize(pointer)
-
# @param pointer [Pointer]
-
# @return [self]
-
# The pointer's release() class method will be invoked at GC time.
-
#
-
# @note The safest, and therefore preferred, calling
-
# idiom is to pass a Method as the second parameter. Example usage:
-
#
-
# class PointerHelper
-
# def self.release(pointer)
-
# ...
-
# end
-
# end
-
#
-
# p = AutoPointer.new(other_pointer, PointerHelper.method(:release))
-
#
-
# The above code will cause PointerHelper#release to be invoked at GC time.
-
#
-
# @note
-
# The last calling idiom (only one parameter) is generally only
-
# going to be useful if you subclass {AutoPointer}, and override
-
# #release, which by default does nothing.
-
1
def initialize(ptr, proc=nil, &block)
-
super(ptr.type_size, ptr)
-
raise TypeError, "Invalid pointer" if ptr.nil? || !ptr.kind_of?(Pointer) \
-
|| ptr.kind_of?(MemoryPointer) || ptr.kind_of?(AutoPointer)
-
-
@releaser = if proc
-
if not proc.respond_to?(:call)
-
raise RuntimeError.new("proc must be callable")
-
end
-
CallableReleaser.new(ptr, proc)
-
-
else
-
if not self.class.respond_to?(:release)
-
raise RuntimeError.new("no release method defined")
-
end
-
DefaultReleaser.new(ptr, self.class)
-
end
-
-
ObjectSpace.define_finalizer(self, @releaser)
-
self
-
end
-
-
# @return [nil]
-
# Free the pointer.
-
1
def free
-
@releaser.free
-
end
-
-
# @param [Boolean] autorelease
-
# @return [Boolean] +autorelease+
-
# Set +autorelease+ property. See {Pointer Autorelease section at Pointer}.
-
1
def autorelease=(autorelease)
-
@releaser.autorelease=(autorelease)
-
end
-
-
# @return [Boolean] +autorelease+
-
# Get +autorelease+ property. See {Pointer Autorelease section at Pointer}.
-
1
def autorelease?
-
@releaser.autorelease
-
end
-
-
# @abstract Base class for {AutoPointer}'s releasers.
-
#
-
# All subclasses of Releaser should define a +#release(ptr)+ method.
-
# A releaser is an object in charge of release an {AutoPointer}.
-
1
class Releaser
-
1
attr_accessor :autorelease
-
-
# @param [Pointer] ptr
-
# @param [#call] proc
-
# @return [nil]
-
# A new instance of Releaser.
-
1
def initialize(ptr, proc)
-
@ptr = ptr
-
@proc = proc
-
@autorelease = true
-
end
-
-
# @return [nil]
-
# Free pointer.
-
1
def free
-
if @ptr
-
release(@ptr)
-
@autorelease = false
-
@ptr = nil
-
@proc = nil
-
end
-
end
-
-
# @param args
-
# Release pointer if +autorelease+ is set.
-
1
def call(*args)
-
release(@ptr) if @autorelease && @ptr
-
end
-
end
-
-
# DefaultReleaser is a {Releaser} used when an {AutoPointer} is defined
-
# without Proc or Method. In this case, the pointer to release must be of
-
# a class derived from AutoPointer with a {release} class method.
-
1
class DefaultReleaser < Releaser
-
# @param [Pointer] ptr
-
# @return [nil]
-
# Release +ptr+ using the {release} class method of its class.
-
1
def release(ptr)
-
@proc.release(ptr)
-
end
-
end
-
-
# CallableReleaser is a {Releaser} used when an {AutoPointer} is defined with a
-
# Proc or a Method.
-
1
class CallableReleaser < Releaser
-
# Release +ptr+ by using Proc or Method defined at +ptr+
-
# {AutoPointer#initialize initialization}.
-
#
-
# @param [Pointer] ptr
-
# @return [nil]
-
1
def release(ptr)
-
@proc.call(ptr)
-
end
-
end
-
-
# Return native type of AutoPointer.
-
#
-
# Override {DataConverter#native_type}.
-
# @return [Type::POINTER]
-
# @raise {RuntimeError} if class does not implement a +#release+ method
-
1
def self.native_type
-
if not self.respond_to?(:release)
-
raise RuntimeError.new("no release method defined for #{self.inspect}")
-
end
-
Type::POINTER
-
end
-
-
# Create a new AutoPointer.
-
#
-
# Override {DataConverter#from_native}.
-
# @overload self.from_native(ptr, ctx)
-
# @param [Pointer] ptr
-
# @param ctx not used. Please set +nil+.
-
# @return [AutoPointer]
-
1
def self.from_native(val, ctx)
-
self.new(val)
-
end
-
end
-
-
end
-
#
-
# All the code from this file is now implemented in C. This file remains
-
# to satisfy any leftover require 'ffi/callback' in user code
-
#
-
#
-
# Copyright (C) 2008-2010 Wayne Meissner
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.#
-
-
1
module FFI
-
# This module is used to extend somes classes and give then a common API.
-
#
-
# Most of methods defined here must be overriden.
-
1
module DataConverter
-
# Get native type.
-
#
-
# @overload native_type(type)
-
# @param [String, Symbol, Type] type
-
# @return [Type]
-
# Get native type from +type+.
-
#
-
# @overload native_type
-
# @raise {NotImplementedError} This method must be overriden.
-
1
def native_type(type = nil)
-
2
if type
-
1
@native_type = FFI.find_type(type)
-
else
-
1
native_type = @native_type
-
1
unless native_type
-
raise NotImplementedError, 'native_type method not overridden and no native_type set'
-
end
-
1
native_type
-
end
-
end
-
-
# Convert to a native type.
-
1
def to_native(value, ctx)
-
value
-
end
-
-
# Convert from a native type.
-
1
def from_native(value, ctx)
-
value
-
end
-
end
-
end
-
#
-
# Copyright (C) 2009, 2010 Wayne Meissner
-
# Copyright (C) 2009 Luc Heinrich
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
#
-
-
1
module FFI
-
-
# An instance of this class permits to manage {Enum}s. In fact, Enums is a collection of {Enum}s.
-
1
class Enums
-
-
# @return [nil]
-
1
def initialize
-
1
@all_enums = Array.new
-
1
@tagged_enums = Hash.new
-
1
@symbol_map = Hash.new
-
end
-
-
# @param [Enum] enum
-
# Add an {Enum} to the collection.
-
1
def <<(enum)
-
4
@all_enums << enum
-
4
@tagged_enums[enum.tag] = enum unless enum.tag.nil?
-
4
@symbol_map.merge!(enum.symbol_map)
-
end
-
-
# @param query enum tag or part of an enum name
-
# @return [Enum]
-
# Find a {Enum} in collection.
-
1
def find(query)
-
if @tagged_enums.has_key?(query)
-
@tagged_enums[query]
-
else
-
@all_enums.detect { |enum| enum.symbols.include?(query) }
-
end
-
end
-
-
# @param symbol a symbol to find in merge symbol maps of all enums.
-
# @return a symbol
-
1
def __map_symbol(symbol)
-
@symbol_map[symbol]
-
end
-
-
end
-
-
# Represents a C enum.
-
#
-
# For a C enum:
-
# enum fruits {
-
# apple,
-
# banana,
-
# orange,
-
# pineapple
-
# };
-
# are defined this vocabulary:
-
# * a _symbol_ is a word from the enumeration (ie. _apple_, by example);
-
# * a _value_ is the value of a symbol in the enumeration (by example, apple has value _0_ and banana _1_).
-
1
class Enum
-
1
include DataConverter
-
-
1
attr_reader :tag
-
1
attr_reader :native_type
-
-
# @overload initialize(info, tag=nil)
-
# @param [nil, Enumerable] info
-
# @param [nil, Symbol] tag enum tag
-
# @overload initialize(native_type, info, tag=nil)
-
# @param [FFI::Type] native_type Native type for new Enum
-
# @param [nil, Enumerable] info symbols and values for new Enum
-
# @param [nil, Symbol] tag name of new Enum
-
1
def initialize(*args)
-
4
@native_type = args.first.kind_of?(FFI::Type) ? args.shift : Type::INT
-
4
info, @tag = *args
-
4
@kv_map = Hash.new
-
4
unless info.nil?
-
4
last_cst = nil
-
4
value = 0
-
4
info.each do |i|
-
19
case i
-
when Symbol
-
19
raise ArgumentError, "duplicate enum key" if @kv_map.has_key?(i)
-
19
@kv_map[i] = value
-
19
last_cst = i
-
19
value += 1
-
when Integer
-
@kv_map[last_cst] = i
-
value = i+1
-
end
-
end
-
end
-
4
@vk_map = @kv_map.invert
-
end
-
-
# @return [Array] enum symbol names
-
1
def symbols
-
2
@kv_map.keys
-
end
-
-
# Get a symbol or a value from the enum.
-
# @overload [](query)
-
# Get enum value from symbol.
-
# @param [Symbol] query
-
# @return [Integer]
-
# @overload [](query)
-
# Get enum symbol from value.
-
# @param [Integer] query
-
# @return [Symbol]
-
1
def [](query)
-
2
case query
-
when Symbol
-
2
@kv_map[query]
-
when Integer
-
@vk_map[query]
-
end
-
end
-
1
alias find []
-
-
# Get the symbol map.
-
# @return [Hash]
-
1
def symbol_map
-
4
@kv_map
-
end
-
-
1
alias to_h symbol_map
-
1
alias to_hash symbol_map
-
-
# @param [Symbol, Integer, #to_int] val
-
# @param ctx unused
-
# @return [Integer] value of a enum symbol
-
1
def to_native(val, ctx)
-
2
@kv_map[val] || if val.is_a?(Integer)
-
2
val
-
elsif val.respond_to?(:to_int)
-
val.to_int
-
else
-
raise ArgumentError, "invalid enum value, #{val.inspect}"
-
end
-
end
-
-
# @param val
-
# @return symbol name if it exists for +val+.
-
1
def from_native(val, ctx)
-
@vk_map[val] || val
-
end
-
end
-
-
# Represents a C enum whose values are power of 2
-
#
-
# @example
-
# enum {
-
# red = (1<<0),
-
# green = (1<<1),
-
# blue = (1<<2)
-
# }
-
#
-
# Contrary to classical enums, bitmask values are usually combined
-
# when used.
-
1
class Bitmask < Enum
-
-
# @overload initialize(info, tag=nil)
-
# @param [nil, Enumerable] info symbols and bit rank for new Bitmask
-
# @param [nil, Symbol] tag name of new Bitmask
-
# @overload initialize(native_type, info, tag=nil)
-
# @param [FFI::Type] native_type Native type for new Bitmask
-
# @param [nil, Enumerable] info symbols and bit rank for new Bitmask
-
# @param [nil, Symbol] tag name of new Bitmask
-
1
def initialize(*args)
-
@native_type = args.first.kind_of?(FFI::Type) ? args.shift : Type::INT
-
info, @tag = *args
-
@kv_map = Hash.new
-
unless info.nil?
-
last_cst = nil
-
value = 0
-
info.each do |i|
-
case i
-
when Symbol
-
raise ArgumentError, "duplicate bitmask key" if @kv_map.has_key?(i)
-
@kv_map[i] = 1 << value
-
last_cst = i
-
value += 1
-
when Integer
-
raise ArgumentError, "bitmask index should be positive" if i<0
-
@kv_map[last_cst] = 1 << i
-
value = i+1
-
end
-
end
-
end
-
@vk_map = @kv_map.invert
-
end
-
-
# Get a symbol list or a value from the bitmask
-
# @overload [](*query)
-
# Get bitmask value from symbol list
-
# @param [Symbol] query
-
# @return [Integer]
-
# @overload [](query)
-
# Get bitmaks value from symbol array
-
# @param [Array<Symbol>] query
-
# @return [Integer]
-
# @overload [](*query)
-
# Get a list of bitmask symbols corresponding to
-
# the or reduction of a list of integer
-
# @param [Integer] query
-
# @return [Array<Symbol>]
-
# @overload [](query)
-
# Get a list of bitmask symbols corresponding to
-
# the or reduction of a list of integer
-
# @param [Array<Integer>] query
-
# @return [Array<Symbol>]
-
1
def [](*query)
-
flat_query = query.flatten
-
raise ArgumentError, "query should be homogeneous, #{query.inspect}" unless flat_query.all? { |o| o.is_a?(Symbol) } || flat_query.all? { |o| o.is_a?(Integer) || o.respond_to?(:to_int) }
-
case flat_query[0]
-
when Symbol
-
flat_query.inject(0) do |val, o|
-
v = @kv_map[o]
-
if v then val |= v else val end
-
end
-
when Integer, ->(o) { o.respond_to?(:to_int) }
-
val = flat_query.inject(0) { |mask, o| mask |= o.to_int }
-
@kv_map.select { |_, v| v & val != 0 }.keys
-
end
-
end
-
-
# Get the native value of a bitmask
-
# @overload to_native(query, ctx)
-
# @param [Symbol, Integer, #to_int] query
-
# @param ctx unused
-
# @return [Integer] value of a bitmask
-
# @overload to_native(query, ctx)
-
# @param [Array<Symbol, Integer, #to_int>] query
-
# @param ctx unused
-
# @return [Integer] value of a bitmask
-
1
def to_native(query, ctx)
-
return 0 if query.nil?
-
flat_query = [query].flatten
-
flat_query.inject(0) do |val, o|
-
case o
-
when Symbol
-
v = @kv_map[o]
-
raise ArgumentError, "invalid bitmask value, #{o.inspect}" unless v
-
val |= v
-
when Integer
-
val |= o
-
when ->(obj) { obj.respond_to?(:to_int) }
-
val |= o.to_int
-
else
-
raise ArgumentError, "invalid bitmask value, #{o.inspect}"
-
end
-
end
-
end
-
-
# @param [Integer] val
-
# @param ctx unused
-
# @return [Array<Symbol, Integer>] list of symbol names corresponding to val, plus an optional remainder if some bits don't match any constant
-
1
def from_native(val, ctx)
-
list = @kv_map.select { |_, v| v & val != 0 }.keys
-
# If there are unmatch flags,
-
# return them in an integer,
-
# else information can be lost.
-
# Similar to Enum behavior.
-
remainder = val ^ list.inject(0) do |tmp, o|
-
v = @kv_map[o]
-
if v then tmp |= v else tmp end
-
end
-
list.push remainder unless remainder == 0
-
return list
-
end
-
end
-
end
-
#
-
# Copyright (C) 2008-2010 Wayne Meissner
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.#
-
-
1
module FFI
-
# @return (see FFI::LastError.error)
-
# @see FFI::LastError.error
-
1
def self.errno
-
FFI::LastError.error
-
end
-
# @param error (see FFI::LastError.error=)
-
# @return (see FFI::LastError.error=)
-
# @see FFI::LastError.error=
-
1
def self.errno=(error)
-
FFI::LastError.error = error
-
end
-
end
-
#
-
# Copyright (C) 2008-2010 JRuby project
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
1
require 'ffi/platform'
-
1
require 'ffi/data_converter'
-
1
require 'ffi/types'
-
1
require 'ffi/library'
-
1
require 'ffi/errno'
-
1
require 'ffi/pointer'
-
1
require 'ffi/memorypointer'
-
1
require 'ffi/struct'
-
1
require 'ffi/union'
-
1
require 'ffi/managedstruct'
-
1
require 'ffi/callback'
-
1
require 'ffi/io'
-
1
require 'ffi/autopointer'
-
1
require 'ffi/variadic'
-
1
require 'ffi/enum'
-
1
require 'ffi/version'
-
#
-
# Copyright (C) 2008, 2009 Wayne Meissner
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.#
-
-
1
module FFI
-
-
# This module implements a couple of class methods to play with IO.
-
1
module IO
-
# @param [Integer] fd file decriptor
-
# @param [String] mode mode string
-
# @return [::IO]
-
# Synonym for IO::for_fd.
-
1
def self.for_fd(fd, mode = "r")
-
::IO.for_fd(fd, mode)
-
end
-
-
# @param [#read] io io to read from
-
# @param [AbstractMemory] buf destination for data read from +io+
-
# @param [nil, Numeric] len maximul number of bytes to read from +io+. If +nil+,
-
# read until end of file.
-
# @return [Numeric] length really read, in bytes
-
#
-
# A version of IO#read that reads data from an IO and put then into a native buffer.
-
#
-
# This will be optimized at some future time to eliminate the double copy.
-
#
-
1
def self.native_read(io, buf, len)
-
tmp = io.read(len)
-
return -1 unless tmp
-
buf.put_bytes(0, tmp)
-
tmp.length
-
end
-
-
end
-
end
-
-
#
-
# Copyright (C) 2008-2010 Wayne Meissner
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.#
-
-
1
module FFI
-
1
CURRENT_PROCESS = USE_THIS_PROCESS_AS_LIBRARY = Object.new
-
-
# @param [#to_s] lib library name
-
# @return [String] library name formatted for current platform
-
# Transform a generic library name to a platform library name
-
# @example
-
# # Linux
-
# FFI.map_library_name 'c' # -> "libc.so.6"
-
# FFI.map_library_name 'jpeg' # -> "libjpeg.so"
-
# # Windows
-
# FFI.map_library_name 'c' # -> "msvcrt.dll"
-
# FFI.map_library_name 'jpeg' # -> "jpeg.dll"
-
1
def self.map_library_name(lib)
-
# Mangle the library name to reflect the native library naming conventions
-
1
lib = Library::LIBC if lib == 'c'
-
-
1
if lib && File.basename(lib) == lib
-
lib = Platform::LIBPREFIX + lib unless lib =~ /^#{Platform::LIBPREFIX}/
-
r = Platform::IS_WINDOWS || Platform::IS_MAC ? "\\.#{Platform::LIBSUFFIX}$" : "\\.so($|\\.[1234567890]+)"
-
lib += ".#{Platform::LIBSUFFIX}" unless lib =~ /#{r}/
-
end
-
-
1
lib
-
end
-
-
# Exception raised when a function is not found in libraries
-
1
class NotFoundError < LoadError
-
1
def initialize(function, *libraries)
-
super("Function '#{function}' not found in [#{libraries[0].nil? ? 'current process' : libraries.join(", ")}]")
-
end
-
end
-
-
# This module is the base to use native functions.
-
#
-
# A basic usage may be:
-
# require 'ffi'
-
#
-
# module Hello
-
# extend FFI::Library
-
# ffi_lib FFI::Library::LIBC
-
# attach_function 'puts', [ :string ], :int
-
# end
-
#
-
# Hello.puts("Hello, World")
-
#
-
#
-
1
module Library
-
1
CURRENT_PROCESS = FFI::CURRENT_PROCESS
-
1
LIBC = FFI::Platform::LIBC
-
-
# @param mod extended object
-
# @return [nil]
-
# @raise {RuntimeError} if +mod+ is not a Module
-
# Test if extended object is a Module. If not, raise RuntimeError.
-
1
def self.extended(mod)
-
1
raise RuntimeError.new("must only be extended by module") unless mod.kind_of?(Module)
-
end
-
-
-
# @param [Array] names names of libraries to load
-
# @return [Array<DynamicLibrary>]
-
# @raise {LoadError} if a library cannot be opened
-
# Load native libraries.
-
1
def ffi_lib(*names)
-
1
raise LoadError.new("library names list must not be empty") if names.empty?
-
-
1
lib_flags = defined?(@ffi_lib_flags) ? @ffi_lib_flags : FFI::DynamicLibrary::RTLD_LAZY | FFI::DynamicLibrary::RTLD_LOCAL
-
1
ffi_libs = names.map do |name|
-
-
1
if name == FFI::CURRENT_PROCESS
-
FFI::DynamicLibrary.open(nil, FFI::DynamicLibrary::RTLD_LAZY | FFI::DynamicLibrary::RTLD_LOCAL)
-
-
else
-
2
libnames = (name.is_a?(::Array) ? name : [ name ]).map(&:to_s).map { |n| [ n, FFI.map_library_name(n) ].uniq }.flatten.compact
-
1
lib = nil
-
1
errors = {}
-
-
1
libnames.each do |libname|
-
begin
-
1
orig = libname
-
1
lib = FFI::DynamicLibrary.open(libname, lib_flags)
-
1
break if lib
-
-
rescue Exception => ex
-
ldscript = false
-
if ex.message =~ /(([^ \t()])+\.so([^ \t:()])*):([ \t])*(invalid ELF header|file too short|invalid file format)/
-
if File.read($1) =~ /(?:GROUP|INPUT) *\( *([^ \)]+)/
-
libname = $1
-
ldscript = true
-
end
-
end
-
-
if ldscript
-
retry
-
else
-
# TODO better library lookup logic
-
unless libname.start_with?("/") || FFI::Platform.windows?
-
path = ['/usr/lib/','/usr/local/lib/','/opt/local/lib/'].find do |pth|
-
File.exist?(pth + libname)
-
end
-
if path
-
libname = path + libname
-
retry
-
end
-
end
-
-
libr = (orig == libname ? orig : "#{orig} #{libname}")
-
errors[libr] = ex
-
end
-
end
-
end
-
-
1
if lib.nil?
-
raise LoadError.new(errors.values.join(".\n"))
-
end
-
-
# return the found lib
-
1
lib
-
end
-
end
-
-
1
@ffi_libs = ffi_libs
-
end
-
-
# Set the calling convention for {#attach_function} and {#callback}
-
#
-
# @see http://en.wikipedia.org/wiki/Stdcall#stdcall
-
# @note +:stdcall+ is typically used for attaching Windows API functions
-
#
-
# @param [Symbol] convention one of +:default+, +:stdcall+
-
# @return [Symbol] the new calling convention
-
1
def ffi_convention(convention = nil)
-
198
@ffi_convention ||= :default
-
198
@ffi_convention = convention if convention
-
198
@ffi_convention
-
end
-
-
# @see #ffi_lib
-
# @return [Array<FFI::DynamicLibrary>] array of currently loaded FFI libraries
-
# @raise [LoadError] if no libraries have been loaded (using {#ffi_lib})
-
# Get FFI libraries loaded using {#ffi_lib}.
-
1
def ffi_libraries
-
98
raise LoadError.new("no library specified") if !defined?(@ffi_libs) || @ffi_libs.empty?
-
98
@ffi_libs
-
end
-
-
# Flags used in {#ffi_lib}.
-
#
-
# This map allows you to supply symbols to {#ffi_lib_flags} instead of
-
# the actual constants.
-
FlagsMap = {
-
1
:global => DynamicLibrary::RTLD_GLOBAL,
-
:local => DynamicLibrary::RTLD_LOCAL,
-
:lazy => DynamicLibrary::RTLD_LAZY,
-
:now => DynamicLibrary::RTLD_NOW
-
}
-
-
# Sets library flags for {#ffi_lib}.
-
#
-
# @example
-
# ffi_lib_flags(:lazy, :local) # => 5
-
#
-
# @param [Symbol, …] flags (see {FlagsMap})
-
# @return [Fixnum] the new value
-
1
def ffi_lib_flags(*flags)
-
@ffi_lib_flags = flags.inject(0) { |result, f| result | FlagsMap[f] }
-
end
-
-
-
##
-
# @overload attach_function(func, args, returns, options = {})
-
# @example attach function without an explicit name
-
# module Foo
-
# extend FFI::Library
-
# ffi_lib FFI::Library::LIBC
-
# attach_function :malloc, [:size_t], :pointer
-
# end
-
# # now callable via Foo.malloc
-
# @overload attach_function(name, func, args, returns, options = {})
-
# @example attach function with an explicit name
-
# module Bar
-
# extend FFI::Library
-
# ffi_lib FFI::Library::LIBC
-
# attach_function :c_malloc, :malloc, [:size_t], :pointer
-
# end
-
# # now callable via Bar.c_malloc
-
#
-
# Attach C function +func+ to this module.
-
#
-
#
-
# @param [#to_s] name name of ruby method to attach as
-
# @param [#to_s] func name of C function to attach
-
# @param [Array<Symbol>] args an array of types
-
# @param [Symbol] returns type of return value
-
# @option options [Boolean] :blocking (@blocking) set to true if the C function is a blocking call
-
# @option options [Symbol] :convention (:default) calling convention (see {#ffi_convention})
-
# @option options [FFI::Enums] :enums
-
# @option options [Hash] :type_map
-
#
-
# @return [FFI::VariadicInvoker]
-
#
-
# @raise [FFI::NotFoundError] if +func+ cannot be found in the attached libraries (see {#ffi_lib})
-
1
def attach_function(name, func, args, returns = nil, options = nil)
-
98
mname, a2, a3, a4, a5 = name, func, args, returns, options
-
98
cname, arg_types, ret_type, opts = (a4 && (a2.is_a?(String) || a2.is_a?(Symbol))) ? [ a2, a3, a4, a5 ] : [ mname.to_s, a2, a3, a4 ]
-
-
# Convert :foo to the native type
-
239
arg_types = arg_types.map { |e| find_type(e) }
-
options = {
-
98
:convention => ffi_convention,
-
98
:type_map => defined?(@ffi_typedefs) ? @ffi_typedefs : nil,
-
:blocking => defined?(@blocking) && @blocking,
-
98
:enums => defined?(@ffi_enums) ? @ffi_enums : nil,
-
}
-
-
98
@blocking = false
-
98
options.merge!(opts) if opts && opts.is_a?(Hash)
-
-
# Try to locate the function in any of the libraries
-
98
invokers = []
-
98
ffi_libraries.each do |lib|
-
98
if invokers.empty?
-
begin
-
98
function = nil
-
98
function_names(cname, arg_types).find do |fname|
-
98
function = lib.find_function(fname)
-
end
-
98
raise LoadError unless function
-
-
98
invokers << if arg_types.length > 0 && arg_types[arg_types.length - 1] == FFI::NativeType::VARARGS
-
VariadicInvoker.new(function, arg_types, find_type(ret_type), options)
-
-
else
-
98
Function.new(find_type(ret_type), arg_types, function, options)
-
end
-
-
rescue LoadError
-
end
-
end
-
end
-
98
invoker = invokers.compact.shift
-
98
raise FFI::NotFoundError.new(cname.to_s, ffi_libraries.map { |lib| lib.name }) unless invoker
-
-
98
invoker.attach(self, mname.to_s)
-
98
invoker
-
end
-
-
# @param [#to_s] name function name
-
# @param [Array] arg_types function's argument types
-
# @return [Array<String>]
-
# This function returns a list of possible names to lookup.
-
# @note Function names on windows may be decorated if they are using stdcall. See
-
# * http://en.wikipedia.org/wiki/Name_mangling#C_name_decoration_in_Microsoft_Windows
-
# * http://msdn.microsoft.com/en-us/library/zxk0tw93%28v=VS.100%29.aspx
-
# * http://en.wikibooks.org/wiki/X86_Disassembly/Calling_Conventions#STDCALL
-
# Note that decorated names can be overridden via def files. Also note that the
-
# windows api, although using, doesn't have decorated names.
-
1
def function_names(name, arg_types)
-
98
result = [name.to_s]
-
98
if ffi_convention == :stdcall
-
# Get the size of each parameter
-
size = arg_types.inject(0) do |mem, arg|
-
size = arg.size
-
# The size must be a multiple of 4
-
size += (4 - size) % 4
-
mem + size
-
end
-
-
result << "_#{name.to_s}@#{size}" # win32
-
result << "#{name.to_s}@#{size}" # win64
-
end
-
98
result
-
end
-
-
# @overload attach_variable(mname, cname, type)
-
# @param [#to_s] mname name of ruby method to attach as
-
# @param [#to_s] cname name of C variable to attach
-
# @param [DataConverter, Struct, Symbol, Type] type C variable's type
-
# @example
-
# module Bar
-
# extend FFI::Library
-
# ffi_lib 'my_lib'
-
# attach_variable :c_myvar, :myvar, :long
-
# end
-
# # now callable via Bar.c_myvar
-
# @overload attach_variable(cname, type)
-
# @param [#to_s] mname name of ruby method to attach as
-
# @param [DataConverter, Struct, Symbol, Type] type C variable's type
-
# @example
-
# module Bar
-
# extend FFI::Library
-
# ffi_lib 'my_lib'
-
# attach_variable :myvar, :long
-
# end
-
# # now callable via Bar.myvar
-
# @return [DynamicLibrary::Symbol]
-
# @raise {FFI::NotFoundError} if +cname+ cannot be found in libraries
-
#
-
# Attach C variable +cname+ to this module.
-
1
def attach_variable(mname, a1, a2 = nil)
-
cname, type = a2 ? [ a1, a2 ] : [ mname.to_s, a1 ]
-
address = nil
-
ffi_libraries.each do |lib|
-
begin
-
address = lib.find_variable(cname.to_s)
-
break unless address.nil?
-
rescue LoadError
-
end
-
end
-
-
raise FFI::NotFoundError.new(cname, ffi_libraries) if address.nil? || address.null?
-
if type.is_a?(Class) && type < FFI::Struct
-
# If it is a global struct, just attach directly to the pointer
-
s = s = type.new(address) # Assigning twice to suppress unused variable warning
-
self.module_eval <<-code, __FILE__, __LINE__
-
@@ffi_gvar_#{mname} = s
-
def self.#{mname}
-
@@ffi_gvar_#{mname}
-
end
-
code
-
-
else
-
sc = Class.new(FFI::Struct)
-
sc.layout :gvar, find_type(type)
-
s = sc.new(address)
-
#
-
# Attach to this module as mname/mname=
-
#
-
self.module_eval <<-code, __FILE__, __LINE__
-
@@ffi_gvar_#{mname} = s
-
def self.#{mname}
-
@@ffi_gvar_#{mname}[:gvar]
-
end
-
def self.#{mname}=(value)
-
@@ffi_gvar_#{mname}[:gvar] = value
-
end
-
code
-
-
end
-
-
address
-
end
-
-
-
# @overload callback(name, params, ret)
-
# @param name callback name to add to type map
-
# @param [Array] params array of parameters' types
-
# @param [DataConverter, Struct, Symbol, Type] ret callback return type
-
# @overload callback(params, ret)
-
# @param [Array] params array of parameters' types
-
# @param [DataConverter, Struct, Symbol, Type] ret callback return type
-
# @return [FFI::CallbackInfo]
-
1
def callback(*args)
-
2
raise ArgumentError, "wrong number of arguments" if args.length < 2 || args.length > 3
-
2
name, params, ret = if args.length == 3
-
2
args
-
else
-
[ nil, args[0], args[1] ]
-
end
-
-
7
native_params = params.map { |e| find_type(e) }
-
2
raise ArgumentError, "callbacks cannot have variadic parameters" if native_params.include?(FFI::Type::VARARGS)
-
2
options = Hash.new
-
2
options[:convention] = ffi_convention
-
2
options[:enums] = @ffi_enums if defined?(@ffi_enums)
-
2
ret_type = find_type(ret)
-
2
if ret_type == Type::STRING
-
raise TypeError, ":string is not allowed as return type of callbacks"
-
end
-
2
cb = FFI::CallbackInfo.new(ret_type, native_params, options)
-
-
# Add to the symbol -> type map (unless there was no name)
-
2
unless name.nil?
-
2
typedef cb, name
-
end
-
-
2
cb
-
end
-
-
# Register or get an already registered type definition.
-
#
-
# To register a new type definition, +old+ should be a {FFI::Type}. +add+
-
# is in this case the type definition.
-
#
-
# If +old+ is a {DataConverter}, a {Type::Mapped} is returned.
-
#
-
# If +old+ is +:enum+
-
# * and +add+ is an +Array+, a call to {#enum} is made with +add+ as single parameter;
-
# * in others cases, +info+ is used to create a named enum.
-
#
-
# If +old+ is a key for type map, #typedef get +old+ type definition.
-
#
-
# @param [DataConverter, Symbol, Type] old
-
# @param [Symbol] add
-
# @param [Symbol] info
-
# @return [FFI::Enum, FFI::Type]
-
1
def typedef(old, add, info=nil)
-
15
@ffi_typedefs = Hash.new unless defined?(@ffi_typedefs)
-
-
15
@ffi_typedefs[add] = if old.kind_of?(FFI::Type)
-
5
old
-
-
10
elsif @ffi_typedefs.has_key?(old)
-
@ffi_typedefs[old]
-
-
10
elsif old.is_a?(DataConverter)
-
FFI::Type::Mapped.new(old)
-
-
10
elsif old == :enum
-
if add.kind_of?(Array)
-
self.enum(add)
-
else
-
self.enum(info, add)
-
end
-
-
else
-
10
FFI.find_type(old)
-
end
-
end
-
-
1
private
-
# Generic enum builder
-
# @param [Class] klass can be one of FFI::Enum or FFI::Bitmask
-
# @param args (see #enum or #bitmask)
-
1
def generic_enum(klass, *args)
-
4
native_type = args.first.kind_of?(FFI::Type) ? args.shift : nil
-
4
name, values = if args[0].kind_of?(Symbol) && args[1].kind_of?(Array)
-
[ args[0], args[1] ]
-
4
elsif args[0].kind_of?(Array)
-
[ nil, args[0] ]
-
else
-
4
[ nil, args ]
-
end
-
4
@ffi_enums = FFI::Enums.new unless defined?(@ffi_enums)
-
4
@ffi_enums << (e = native_type ? klass.new(native_type, values, name) : klass.new(values, name))
-
-
# If called with a name, add a typedef alias
-
4
typedef(e, name) if name
-
4
e
-
end
-
-
1
public
-
# @overload enum(name, values)
-
# Create a named enum.
-
# @example
-
# enum :foo, [:zero, :one, :two] # named enum
-
# @param [Symbol] name name for new enum
-
# @param [Array] values values for enum
-
# @overload enum(*args)
-
# Create an unnamed enum.
-
# @example
-
# enum :zero, :one, :two # unnamed enum
-
# @param args values for enum
-
# @overload enum(values)
-
# Create an unnamed enum.
-
# @example
-
# enum [:zero, :one, :two] # unnamed enum, equivalent to above example
-
# @param [Array] values values for enum
-
# @overload enum(native_type, name, values)
-
# Create a named enum and specify the native type.
-
# @example
-
# enum FFI::Type::UINT64, :foo, [:zero, :one, :two] # named enum
-
# @param [FFI::Type] native_type native type for new enum
-
# @param [Symbol] name name for new enum
-
# @param [Array] values values for enum
-
# @overload enum(native_type, *args)
-
# Create an unnamed enum and specify the native type.
-
# @example
-
# enum FFI::Type::UINT64, :zero, :one, :two # unnamed enum
-
# @param [FFI::Type] native_type native type for new enum
-
# @param args values for enum
-
# @overload enum(native_type, values)
-
# Create an unnamed enum and specify the native type.
-
# @example
-
# enum Type::UINT64, [:zero, :one, :two] # unnamed enum, equivalent to above example
-
# @param [FFI::Type] native_type native type for new enum
-
# @param [Array] values values for enum
-
# @return [FFI::Enum]
-
# Create a new {FFI::Enum}.
-
1
def enum(*args)
-
4
generic_enum(FFI::Enum, *args)
-
end
-
-
# @overload bitmask(name, values)
-
# Create a named bitmask
-
# @example
-
# bitmask :foo, [:red, :green, :blue] # bits 0,1,2 are used
-
# bitmask :foo, [:red, :green, 5, :blue] # bits 0,5,6 are used
-
# @param [Symbol] name for new bitmask
-
# @param [Array<Symbol, Integer>] values for new bitmask
-
# @overload bitmask(*args)
-
# Create an unamed bitmask
-
# @example
-
# bm = bitmask :red, :green, :blue # bits 0,1,2 are used
-
# bm = bitmask :red, :green, 5, blue # bits 0,5,6 are used
-
# @param [Symbol, Integer] args values for new bitmask
-
# @overload bitmask(values)
-
# Create an unamed bitmask
-
# @example
-
# bm = bitmask [:red, :green, :blue] # bits 0,1,2 are used
-
# bm = bitmask [:red, :green, 5, blue] # bits 0,5,6 are used
-
# @param [Array<Symbol, Integer>] values for new bitmask
-
# @overload bitmask(native_type, name, values)
-
# Create a named enum and specify the native type.
-
# @example
-
# bitmask FFI::Type::UINT64, :foo, [:red, :green, :blue]
-
# @param [FFI::Type] native_type native type for new bitmask
-
# @param [Symbol] name for new bitmask
-
# @param [Array<Symbol, Integer>] values for new bitmask
-
# @overload bitmask(native_type, *args)
-
# @example
-
# bitmask FFI::Type::UINT64, :red, :green, :blue
-
# @param [FFI::Type] native_type native type for new bitmask
-
# @param [Symbol, Integer] args values for new bitmask
-
# @overload bitmask(native_type, values)
-
# Create a named enum and specify the native type.
-
# @example
-
# bitmask FFI::Type::UINT64, [:red, :green, :blue]
-
# @param [FFI::Type] native_type native type for new bitmask
-
# @param [Array<Symbol, Integer>] values for new bitmask
-
# @return [FFI::Bitmask]
-
# Create a new FFI::Bitmask
-
1
def bitmask(*args)
-
generic_enum(FFI::Bitmask, *args)
-
end
-
-
# @param name
-
# @return [FFI::Enum]
-
# Find an enum by name.
-
1
def enum_type(name)
-
@ffi_enums.find(name) if defined?(@ffi_enums)
-
end
-
-
# @param symbol
-
# @return [FFI::Enum]
-
# Find an enum by a symbol it contains.
-
1
def enum_value(symbol)
-
@ffi_enums.__map_symbol(symbol)
-
end
-
-
# @param [DataConverter, Type, Struct, Symbol] t type to find
-
# @return [Type]
-
# Find a type definition.
-
1
def find_type(t)
-
275
if t.kind_of?(Type)
-
4
t
-
-
271
elsif defined?(@ffi_typedefs) && @ffi_typedefs.has_key?(t)
-
127
@ffi_typedefs[t]
-
-
144
elsif t.is_a?(Class) && t < Struct
-
Type::POINTER
-
-
144
elsif t.is_a?(DataConverter)
-
# Add a typedef so next time the converter is used, it hits the cache
-
3
typedef Type::Mapped.new(t), t
-
-
end || FFI.find_type(t)
-
end
-
end
-
end
-
# Copyright (C) 2008 Mike Dalessio
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
1
module FFI
-
#
-
# FFI::ManagedStruct allows custom garbage-collection of your FFI::Structs.
-
#
-
# The typical use case would be when interacting with a library
-
# that has a nontrivial memory management design, such as a linked
-
# list or a binary tree.
-
#
-
# When the {Struct} instance is garbage collected, FFI::ManagedStruct will
-
# invoke the class's release() method during object finalization.
-
#
-
# @example Example usage:
-
# module MyLibrary
-
# ffi_lib "libmylibrary"
-
# attach_function :new_dlist, [], :pointer
-
# attach_function :destroy_dlist, [:pointer], :void
-
# end
-
#
-
# class DoublyLinkedList < FFI::ManagedStruct
-
# @@@
-
# struct do |s|
-
# s.name 'struct dlist'
-
# s.include 'dlist.h'
-
# s.field :head, :pointer
-
# s.field :tail, :pointer
-
# end
-
# @@@
-
#
-
# def self.release ptr
-
# MyLibrary.destroy_dlist(ptr)
-
# end
-
# end
-
#
-
# begin
-
# ptr = DoublyLinkedList.new(MyLibrary.new_dlist)
-
# # do something with the list
-
# end
-
# # struct is out of scope, and will be GC'd using DoublyLinkedList#release
-
#
-
#
-
1
class ManagedStruct < FFI::Struct
-
-
# @overload initialize(pointer)
-
# @param [Pointer] pointer
-
# Create a new ManagedStruct which will invoke the class method #release on
-
# @overload initialize
-
# A new instance of FFI::ManagedStruct.
-
1
def initialize(pointer=nil)
-
raise NoMethodError, "release() not implemented for class #{self}" unless self.class.respond_to? :release
-
raise ArgumentError, "Must supply a pointer to memory for the Struct" unless pointer
-
super AutoPointer.new(pointer, self.class.method(:release))
-
end
-
-
end
-
end
-
# This class is now implemented in C
-
#
-
# Copyright (C) 2008, 2009 Wayne Meissner
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.#
-
-
1
require 'rbconfig'
-
1
module FFI
-
1
class PlatformError < LoadError; end
-
-
# This module defines different constants and class methods to play with
-
# various platforms.
-
1
module Platform
-
1
OS = case RbConfig::CONFIG['host_os'].downcase
-
when /linux/
-
1
"linux"
-
when /darwin/
-
"darwin"
-
when /freebsd/
-
"freebsd"
-
when /netbsd/
-
"netbsd"
-
when /openbsd/
-
"openbsd"
-
when /dragonfly/
-
"dragonflybsd"
-
when /sunos|solaris/
-
"solaris"
-
when /mingw|mswin/
-
"windows"
-
else
-
RbConfig::CONFIG['host_os'].downcase
-
end
-
-
1
OSVERSION = RbConfig::CONFIG['host_os'].gsub(/[^\d]/, '').to_i
-
-
1
CPU = RbConfig::CONFIG['host_cpu']
-
-
1
ARCH = case CPU.downcase
-
when /amd64|x86_64|x64/
-
1
"x86_64"
-
when /i?86|x86|i86pc/
-
"i386"
-
when /ppc64|powerpc64/
-
"powerpc64"
-
when /ppc|powerpc/
-
"powerpc"
-
when /sparcv9|sparc64/
-
"sparcv9"
-
else
-
case RbConfig::CONFIG['host_cpu']
-
when /^arm/
-
"arm"
-
else
-
RbConfig::CONFIG['host_cpu']
-
end
-
end
-
-
1
private
-
# @param [String) os
-
# @return [Boolean]
-
# Test if current OS is +os+.
-
1
def self.is_os(os)
-
8
OS == os
-
end
-
-
1
IS_GNU = defined?(GNU_LIBC)
-
1
IS_LINUX = is_os("linux")
-
1
IS_MAC = is_os("darwin")
-
1
IS_FREEBSD = is_os("freebsd")
-
1
IS_NETBSD = is_os("netbsd")
-
1
IS_OPENBSD = is_os("openbsd")
-
1
IS_DRAGONFLYBSD = is_os("dragonfly")
-
1
IS_SOLARIS = is_os("solaris")
-
1
IS_WINDOWS = is_os("windows")
-
1
IS_BSD = IS_MAC || IS_FREEBSD || IS_NETBSD || IS_OPENBSD || IS_DRAGONFLYBSD
-
-
# Add the version for known ABI breaks
-
1
name_version = "12" if IS_FREEBSD && OSVERSION >= 12 # 64-bit inodes
-
-
1
NAME = "#{ARCH}-#{OS}#{name_version}"
-
1
CONF_DIR = File.join(File.dirname(__FILE__), 'platform', NAME)
-
-
1
public
-
-
1
LIBPREFIX = case OS
-
when /windows|msys/
-
''
-
when /cygwin/
-
'cyg'
-
else
-
1
'lib'
-
end
-
-
1
LIBSUFFIX = case OS
-
when /darwin/
-
'dylib'
-
when /linux|bsd|solaris/
-
1
'so'
-
when /windows|cygwin|msys/
-
'dll'
-
else
-
# Punt and just assume a sane unix (i.e. anything but AIX)
-
'so'
-
end
-
-
1
LIBC = if IS_WINDOWS
-
if RbConfig::CONFIG['host_os'] =~ /mingw/i
-
RbConfig::CONFIG['RUBY_SO_NAME'].split('-')[-2] + '.dll'
-
else
-
"ucrtbase.dll"
-
end
-
1
elsif IS_GNU
-
GNU_LIBC
-
1
elsif OS == 'cygwin'
-
"cygwin1.dll"
-
1
elsif OS == 'msys'
-
# Not sure how msys 1.0 behaves, tested on MSYS2.
-
"msys-2.0.dll"
-
else
-
1
"#{LIBPREFIX}c.#{LIBSUFFIX}"
-
end
-
-
# Test if current OS is a *BSD (include MAC)
-
# @return [Boolean]
-
1
def self.bsd?
-
IS_BSD
-
end
-
-
# Test if current OS is Windows
-
# @return [Boolean]
-
1
def self.windows?
-
IS_WINDOWS
-
end
-
-
# Test if current OS is Mac OS
-
# @return [Boolean]
-
1
def self.mac?
-
IS_MAC
-
end
-
-
# Test if current OS is Solaris (Sun OS)
-
# @return [Boolean]
-
1
def self.solaris?
-
IS_SOLARIS
-
end
-
-
# Test if current OS is a unix OS
-
# @return [Boolean]
-
1
def self.unix?
-
!IS_WINDOWS
-
end
-
end
-
end
-
-
#
-
# Copyright (C) 2008, 2009 Wayne Meissner
-
# Copyright (c) 2007, 2008 Evan Phoenix
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
#
-
-
1
require 'ffi/platform'
-
-
# NOTE: all method definitions in this file are conditional on
-
# whether they are not already defined. This is needed because
-
# some Ruby implementations (e.g., TruffleRuby) might already
-
# provide these methods due to using FFI internally, and we
-
# should not override them to avoid warnings.
-
-
1
module FFI
-
1
class Pointer
-
-
# Pointer size
-
1
SIZE = Platform::ADDRESS_SIZE / 8 unless const_defined?(:SIZE)
-
-
# Return the size of a pointer on the current platform, in bytes
-
# @return [Numeric]
-
def self.size
-
SIZE
-
1
end unless respond_to?(:size)
-
-
# @param [nil,Numeric] len length of string to return
-
# @return [String]
-
# Read pointer's contents as a string, or the first +len+ bytes of the
-
# equivalent string if +len+ is not +nil+.
-
def read_string(len=nil)
-
if len
-
return ''.b if len == 0
-
get_bytes(0, len)
-
else
-
get_string(0)
-
end
-
1
end unless method_defined?(:read_string)
-
-
# @param [Numeric] len length of string to return
-
# @return [String]
-
# Read the first +len+ bytes of pointer's contents as a string.
-
#
-
# Same as:
-
# ptr.read_string(len) # with len not nil
-
def read_string_length(len)
-
get_bytes(0, len)
-
1
end unless method_defined?(:read_string_length)
-
-
# @return [String]
-
# Read pointer's contents as a string.
-
#
-
# Same as:
-
# ptr.read_string # with no len
-
def read_string_to_null
-
get_string(0)
-
1
end unless method_defined?(:read_string_to_null)
-
-
# @param [String] str string to write
-
# @param [Numeric] len length of string to return
-
# @return [self]
-
# Write +len+ first bytes of +str+ in pointer's contents.
-
#
-
# Same as:
-
# ptr.write_string(str, len) # with len not nil
-
def write_string_length(str, len)
-
put_bytes(0, str, 0, len)
-
1
end unless method_defined?(:write_string_length)
-
-
# @param [String] str string to write
-
# @param [Numeric] len length of string to return
-
# @return [self]
-
# Write +str+ in pointer's contents, or first +len+ bytes if
-
# +len+ is not +nil+.
-
def write_string(str, len=nil)
-
len = str.bytesize unless len
-
# Write the string data without NUL termination
-
put_bytes(0, str, 0, len)
-
1
end unless method_defined?(:write_string)
-
-
# @param [Type] type type of data to read from pointer's contents
-
# @param [Symbol] reader method to send to +self+ to read +type+
-
# @param [Numeric] length
-
# @return [Array]
-
# Read an array of +type+ of length +length+.
-
# @example
-
# ptr.read_array_of_type(TYPE_UINT8, :read_uint8, 4) # -> [1, 2, 3, 4]
-
def read_array_of_type(type, reader, length)
-
ary = []
-
size = FFI.type_size(type)
-
tmp = self
-
length.times { |j|
-
ary << tmp.send(reader)
-
tmp += size unless j == length-1 # avoid OOB
-
}
-
ary
-
1
end unless method_defined?(:read_array_of_type)
-
-
# @param [Type] type type of data to write to pointer's contents
-
# @param [Symbol] writer method to send to +self+ to write +type+
-
# @param [Array] ary
-
# @return [self]
-
# Write +ary+ in pointer's contents as +type+.
-
# @example
-
# ptr.write_array_of_type(TYPE_UINT8, :put_uint8, [1, 2, 3 ,4])
-
def write_array_of_type(type, writer, ary)
-
size = FFI.type_size(type)
-
ary.each_with_index { |val, i|
-
break unless i < self.size
-
self.send(writer, i * size, val)
-
}
-
self
-
1
end unless method_defined?(:write_array_of_type)
-
-
# @return [self]
-
def to_ptr
-
self
-
1
end unless method_defined?(:to_ptr)
-
-
# @param [Symbol,Type] type of data to read
-
# @return [Object]
-
# Read pointer's contents as +type+
-
#
-
# Same as:
-
# ptr.get(type, 0)
-
def read(type)
-
get(type, 0)
-
1
end unless method_defined?(:read)
-
-
# @param [Symbol,Type] type of data to read
-
# @param [Object] value to write
-
# @return [nil]
-
# Write +value+ of type +type+ to pointer's content
-
#
-
# Same as:
-
# ptr.put(type, 0)
-
def write(type, value)
-
put(type, 0, value)
-
1
end unless method_defined?(:write)
-
end
-
end
-
#
-
# Copyright (C) 2008-2010 Wayne Meissner
-
# Copyright (C) 2008, 2009 Andrea Fazzi
-
# Copyright (C) 2008, 2009 Luc Heinrich
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
#
-
-
1
require 'ffi/platform'
-
1
require 'ffi/struct_layout'
-
1
require 'ffi/struct_layout_builder'
-
1
require 'ffi/struct_by_reference'
-
-
1
module FFI
-
-
1
class Struct
-
-
# Get struct size
-
# @return [Numeric]
-
1
def size
-
self.class.size
-
end
-
-
# @return [Fixnum] Struct alignment
-
1
def alignment
-
self.class.alignment
-
end
-
1
alias_method :align, :alignment
-
-
# (see FFI::StructLayout#offset_of)
-
1
def offset_of(name)
-
self.class.offset_of(name)
-
end
-
-
# (see FFI::StructLayout#members)
-
1
def members
-
self.class.members
-
end
-
-
# @return [Array]
-
# Get array of values from Struct fields.
-
1
def values
-
members.map { |m| self[m] }
-
end
-
-
# (see FFI::StructLayout#offsets)
-
1
def offsets
-
self.class.offsets
-
end
-
-
# Clear the struct content.
-
# @return [self]
-
1
def clear
-
pointer.clear
-
self
-
end
-
-
# Get {Pointer} to struct content.
-
# @return [AbstractMemory]
-
1
def to_ptr
-
pointer
-
end
-
-
# Get struct size
-
# @return [Numeric]
-
1
def self.size
-
defined?(@layout) ? @layout.size : defined?(@size) ? @size : 0
-
end
-
-
# set struct size
-
# @param [Numeric] size
-
# @return [size]
-
1
def self.size=(size)
-
raise ArgumentError, "Size already set" if defined?(@size) || defined?(@layout)
-
@size = size
-
end
-
-
# @return (see Struct#alignment)
-
1
def self.alignment
-
@layout.alignment
-
end
-
-
# (see FFI::Type#members)
-
1
def self.members
-
@layout.members
-
end
-
-
# (see FFI::StructLayout#offsets)
-
1
def self.offsets
-
@layout.offsets
-
end
-
-
# (see FFI::StructLayout#offset_of)
-
1
def self.offset_of(name)
-
@layout.offset_of(name)
-
end
-
-
1
def self.in
-
ptr(:in)
-
end
-
-
1
def self.out
-
ptr(:out)
-
end
-
-
1
def self.ptr(flags = :inout)
-
4
@ref_data_type ||= Type::Mapped.new(StructByReference.new(self))
-
end
-
-
1
def self.val
-
@val_data_type ||= StructByValue.new(self)
-
end
-
-
1
def self.by_value
-
self.val
-
end
-
-
1
def self.by_ref(flags = :inout)
-
self.ptr(flags)
-
end
-
-
1
class ManagedStructConverter < StructByReference
-
-
# @param [Struct] struct_class
-
1
def initialize(struct_class)
-
super(struct_class)
-
-
raise NoMethodError, "release() not implemented for class #{struct_class}" unless struct_class.respond_to? :release
-
@method = struct_class.method(:release)
-
end
-
-
# @param [Pointer] ptr
-
# @param [nil] ctx
-
# @return [Struct]
-
1
def from_native(ptr, ctx)
-
struct_class.new(AutoPointer.new(ptr, @method))
-
end
-
end
-
-
1
def self.auto_ptr
-
@managed_type ||= Type::Mapped.new(ManagedStructConverter.new(self))
-
end
-
-
-
1
class << self
-
1
public
-
-
# @return [StructLayout]
-
# @overload layout
-
# @return [StructLayout]
-
# Get struct layout.
-
# @overload layout(*spec)
-
# @param [Array<Symbol, Integer>,Array(Hash)] spec
-
# @return [StructLayout]
-
# Create struct layout from +spec+.
-
# @example Creating a layout from an array +spec+
-
# class MyStruct < Struct
-
# layout :field1, :int,
-
# :field2, :pointer,
-
# :field3, :string
-
# end
-
# @example Creating a layout from an array +spec+ with offset
-
# class MyStructWithOffset < Struct
-
# layout :field1, :int,
-
# :field2, :pointer, 6, # set offset to 6 for this field
-
# :field3, :string
-
# end
-
# @example Creating a layout from a hash +spec+
-
# class MyStructFromHash < Struct
-
# layout :field1 => :int,
-
# :field2 => :pointer,
-
# :field3 => :string
-
# end
-
# @example Creating a layout with pointers to functions
-
# class MyFunctionTable < Struct
-
# layout :function1, callback([:int, :int], :int),
-
# :function2, callback([:pointer], :void),
-
# :field3, :string
-
# end
-
1
def layout(*spec)
-
13
warn "[DEPRECATION] Struct layout is already defined for class #{self.inspect}. Redefinition as in #{caller[0]} will be disallowed in ffi-2.0." if defined?(@layout)
-
13
return @layout if spec.size == 0
-
-
13
builder = StructLayoutBuilder.new
-
13
builder.union = self < Union
-
13
builder.packed = @packed if defined?(@packed)
-
13
builder.alignment = @min_alignment if defined?(@min_alignment)
-
-
13
if spec[0].kind_of?(Hash)
-
hash_layout(builder, spec)
-
else
-
13
array_layout(builder, spec)
-
end
-
13
builder.size = @size if defined?(@size) && @size > builder.size
-
13
cspec = builder.build
-
13
@layout = cspec unless self == Struct
-
13
@size = cspec.size
-
13
return cspec
-
end
-
-
-
1
protected
-
-
1
def callback(params, ret)
-
mod = enclosing_module
-
ret_type = find_type(ret, mod)
-
if ret_type == Type::STRING
-
raise TypeError, ":string is not allowed as return type of callbacks"
-
end
-
FFI::CallbackInfo.new(ret_type, params.map { |e| find_type(e, mod) })
-
end
-
-
1
def packed(packed = 1)
-
@packed = packed
-
end
-
1
alias :pack :packed
-
-
1
def aligned(alignment = 1)
-
@min_alignment = alignment
-
end
-
1
alias :align :aligned
-
-
1
def enclosing_module
-
begin
-
117
mod = self.name.split("::")[0..-2].inject(Object) { |obj, c| obj.const_get(c) }
-
39
if mod.respond_to?(:find_type) && (mod.is_a?(FFI::Library) || mod < FFI::Struct)
-
39
mod
-
end
-
rescue Exception
-
nil
-
end
-
end
-
-
-
1
def find_field_type(type, mod = enclosing_module)
-
39
if type.kind_of?(Class) && type < Struct
-
10
FFI::Type::Struct.new(type)
-
-
29
elsif type.kind_of?(Class) && type < FFI::StructLayout::Field
-
type
-
-
29
elsif type.kind_of?(::Array)
-
FFI::Type::Array.new(find_field_type(type[0]), type[1])
-
-
else
-
29
find_type(type, mod)
-
end
-
end
-
-
1
def find_type(type, mod = enclosing_module)
-
29
if mod
-
29
mod.find_type(type)
-
end || FFI.find_type(type)
-
end
-
-
1
private
-
-
# @param [StructLayoutBuilder] builder
-
# @param [Hash] spec
-
# @return [builder]
-
# Add hash +spec+ to +builder+.
-
1
def hash_layout(builder, spec)
-
spec[0].each do |name, type|
-
builder.add name, find_field_type(type), nil
-
end
-
end
-
-
# @param [StructLayoutBuilder] builder
-
# @param [Array<Symbol, Integer>] spec
-
# @return [builder]
-
# Add array +spec+ to +builder+.
-
1
def array_layout(builder, spec)
-
13
i = 0
-
13
while i < spec.size
-
39
name, type = spec[i, 2]
-
39
i += 2
-
-
# If the next param is a Integer, it specifies the offset
-
39
if spec[i].kind_of?(Integer)
-
offset = spec[i]
-
i += 1
-
else
-
39
offset = nil
-
end
-
-
39
builder.add name, find_field_type(type), offset
-
end
-
end
-
end
-
end
-
end
-
#
-
# Copyright (C) 2010 Wayne Meissner
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.#
-
-
1
module FFI
-
# This class includes the {FFI::DataConverter} module.
-
1
class StructByReference
-
1
include DataConverter
-
-
1
attr_reader :struct_class
-
-
# @param [Struct] struct_class
-
1
def initialize(struct_class)
-
3
unless Class === struct_class and struct_class < FFI::Struct
-
raise TypeError, 'wrong type (expected subclass of FFI::Struct)'
-
end
-
3
@struct_class = struct_class
-
end
-
-
# Always get {FFI::Type}::POINTER.
-
1
def native_type
-
3
FFI::Type::POINTER
-
end
-
-
# @param [nil, Struct] value
-
# @param [nil] ctx
-
# @return [AbstractMemory] Pointer on +value+.
-
1
def to_native(value, ctx)
-
return Pointer::NULL if value.nil?
-
-
unless @struct_class === value
-
raise TypeError, "wrong argument type #{value.class} (expected #{@struct_class})"
-
end
-
-
value.pointer
-
end
-
-
# @param [AbstractMemory] value
-
# @param [nil] ctx
-
# @return [Struct]
-
# Create a struct from content of memory +value+.
-
1
def from_native(value, ctx)
-
@struct_class.new(value)
-
end
-
end
-
end
-
#
-
# Copyright (C) 2008-2010 Wayne Meissner
-
# Copyright (C) 2008, 2009 Andrea Fazzi
-
# Copyright (C) 2008, 2009 Luc Heinrich
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
#
-
-
1
module FFI
-
-
1
class StructLayout
-
-
# @return [Array<Array(Symbol, Numeric)>
-
# Get an array of tuples (field name, offset of the field).
-
1
def offsets
-
members.map { |m| [ m, self[m].offset ] }
-
end
-
-
# @return [Numeric]
-
# Get the offset of a field.
-
1
def offset_of(field_name)
-
self[field_name].offset
-
end
-
-
# An enum {Field} in a {StructLayout}.
-
1
class Enum < Field
-
-
# @param [AbstractMemory] ptr pointer on a {Struct}
-
# @return [Object]
-
# Get an object of type {#type} from memory pointed by +ptr+.
-
1
def get(ptr)
-
type.find(ptr.get_int(offset))
-
end
-
-
# @param [AbstractMemory] ptr pointer on a {Struct}
-
# @param value
-
# @return [nil]
-
# Set +value+ into memory pointed by +ptr+.
-
1
def put(ptr, value)
-
ptr.put_int(offset, type.find(value))
-
end
-
-
end
-
-
1
class InnerStruct < Field
-
1
def get(ptr)
-
type.struct_class.new(ptr.slice(self.offset, self.size))
-
end
-
-
1
def put(ptr, value)
-
raise TypeError, "wrong value type (expected #{type.struct_class})" unless value.is_a?(type.struct_class)
-
ptr.slice(self.offset, self.size).__copy_from__(value.pointer, self.size)
-
end
-
end
-
-
1
class Mapped < Field
-
1
def initialize(name, offset, type, orig_field)
-
15
super(name, offset, type)
-
15
@orig_field = orig_field
-
end
-
-
1
def get(ptr)
-
type.from_native(@orig_field.get(ptr), nil)
-
end
-
-
1
def put(ptr, value)
-
@orig_field.put(ptr, type.to_native(value, nil))
-
end
-
end
-
end
-
end
-
#
-
# Copyright (C) 2008-2010 Wayne Meissner
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
#
-
-
1
module FFI
-
-
# Build a {StructLayout struct layout}.
-
1
class StructLayoutBuilder
-
1
attr_reader :size
-
1
attr_reader :alignment
-
-
1
def initialize
-
13
@size = 0
-
13
@alignment = 1
-
13
@min_alignment = 1
-
13
@packed = false
-
13
@union = false
-
13
@fields = Array.new
-
end
-
-
# Set size attribute with +size+ only if +size+ is greater than attribute value.
-
# @param [Numeric] size
-
1
def size=(size)
-
@size = size if size > @size
-
end
-
-
# Set alignment attribute with +align+ only if it is greater than attribute value.
-
# @param [Numeric] align
-
1
def alignment=(align)
-
@alignment = align if align > @alignment
-
@min_alignment = align
-
end
-
-
# Set union attribute.
-
# Set to +true+ to build a {Union} instead of a {Struct}.
-
# @param [Boolean] is_union
-
# @return [is_union]
-
1
def union=(is_union)
-
13
@union = is_union
-
end
-
-
# Building a {Union} or a {Struct} ?
-
#
-
# @return [Boolean]
-
#
-
1
def union?
-
@union
-
end
-
-
# Set packed attribute
-
# @overload packed=(packed) Set alignment and packed attributes to
-
# +packed+.
-
#
-
# @param [Fixnum] packed
-
#
-
# @return [packed]
-
# @overload packed=(packed) Set packed attribute.
-
# @param packed
-
#
-
# @return [0,1]
-
#
-
1
def packed=(packed)
-
if packed.is_a?(0.class)
-
@alignment = packed
-
@packed = packed
-
else
-
@packed = packed ? 1 : 0
-
end
-
end
-
-
-
# List of number types
-
NUMBER_TYPES = [
-
1
Type::INT8,
-
Type::UINT8,
-
Type::INT16,
-
Type::UINT16,
-
Type::INT32,
-
Type::UINT32,
-
Type::LONG,
-
Type::ULONG,
-
Type::INT64,
-
Type::UINT64,
-
Type::FLOAT32,
-
Type::FLOAT64,
-
Type::LONGDOUBLE,
-
Type::BOOL,
-
]
-
-
# @param [String, Symbol] name name of the field
-
# @param [Array, DataConverter, Struct, StructLayout::Field, Symbol, Type] type type of the field
-
# @param [Numeric, nil] offset
-
# @return [self]
-
# Add a field to the builder.
-
# @note Setting +offset+ to +nil+ or +-1+ is equivalent to +0+.
-
1
def add(name, type, offset = nil)
-
-
39
if offset.nil? || offset == -1
-
39
offset = @union ? 0 : align(@size, @packed ? [ @packed, type.alignment ].min : [ @min_alignment, type.alignment ].max)
-
end
-
-
#
-
# If a FFI::Type type was passed in as the field arg, try and convert to a StructLayout::Field instance
-
#
-
39
field = type.is_a?(StructLayout::Field) ? type : field_for_type(name, offset, type)
-
39
@fields << field
-
39
@alignment = [ @alignment, field.alignment ].max unless @packed
-
39
@size = [ @size, field.size + (@union ? 0 : field.offset) ].max
-
-
39
return self
-
end
-
-
# @param (see #add)
-
# @return (see #add)
-
# Same as {#add}.
-
# @see #add
-
1
def add_field(name, type, offset = nil)
-
add(name, type, offset)
-
end
-
-
# @param (see #add)
-
# @return (see #add)
-
# Add a struct as a field to the builder.
-
1
def add_struct(name, type, offset = nil)
-
add(name, Type::Struct.new(type), offset)
-
end
-
-
# @param name (see #add)
-
# @param type (see #add)
-
# @param [Numeric] count array length
-
# @param offset (see #add)
-
# @return (see #add)
-
# Add an array as a field to the builder.
-
1
def add_array(name, type, count, offset = nil)
-
add(name, Type::Array.new(type, count), offset)
-
end
-
-
# @return [StructLayout]
-
# Build and return the struct layout.
-
1
def build
-
# Add tail padding if the struct is not packed
-
13
size = @packed ? @size : align(@size, @alignment)
-
-
13
layout = StructLayout.new(@fields, size, @alignment)
-
13
layout.__union! if @union
-
13
layout
-
end
-
-
1
private
-
-
# @param [Numeric] offset
-
# @param [Numeric] align
-
# @return [Numeric]
-
1
def align(offset, align)
-
42
align + ((offset - 1) & ~(align - 1));
-
end
-
-
# @param (see #add)
-
# @return [StructLayout::Field]
-
1
def field_for_type(name, offset, type)
-
field_class = case
-
54
when type.is_a?(Type::Function)
-
StructLayout::Function
-
-
when type.is_a?(Type::Struct)
-
10
StructLayout::InnerStruct
-
-
when type.is_a?(Type::Array)
-
StructLayout::Array
-
-
when type.is_a?(FFI::Enum)
-
StructLayout::Enum
-
-
when NUMBER_TYPES.include?(type)
-
19
StructLayout::Number
-
-
when type == Type::POINTER
-
5
StructLayout::Pointer
-
-
when type == Type::STRING
-
5
StructLayout::String
-
-
when type.is_a?(Class) && type < StructLayout::Field
-
type
-
-
when type.is_a?(DataConverter)
-
return StructLayout::Mapped.new(name, offset, Type::Mapped.new(type), field_for_type(name, offset, type.native_type))
-
-
when type.is_a?(Type::Mapped)
-
15
return StructLayout::Mapped.new(name, offset, type, field_for_type(name, offset, type.native_type))
-
-
else
-
raise TypeError, "invalid struct field type #{type.inspect}"
-
end
-
-
39
field_class.new(name, offset, type)
-
end
-
end
-
-
end
-
#
-
# Copyright (C) 2008-2010 Wayne Meissner
-
# Copyright (c) 2007, 2008 Evan Phoenix
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
#
-
-
# see {file:README}
-
1
module FFI
-
-
# @param [Type, DataConverter, Symbol] old type definition used by {FFI.find_type}
-
# @param [Symbol] add new type definition's name to add
-
# @return [Type]
-
# Add a definition type to type definitions.
-
1
def self.typedef(old, add)
-
134
TypeDefs[add] = self.find_type(old)
-
end
-
-
# (see FFI.typedef)
-
1
def self.add_typedef(old, add)
-
typedef old, add
-
end
-
-
-
# @param [Type, DataConverter, Symbol] name
-
# @param [Hash] type_map if nil, {FFI::TypeDefs} is used
-
# @return [Type]
-
# Find a type in +type_map+ ({FFI::TypeDefs}, by default) from
-
# a type objet, a type name (symbol). If +name+ is a {DataConverter},
-
# a new {Type::Mapped} is created.
-
1
def self.find_type(name, type_map = nil)
-
286
if name.is_a?(Type)
-
1
name
-
-
285
elsif type_map && type_map.has_key?(name)
-
type_map[name]
-
-
285
elsif TypeDefs.has_key?(name)
-
284
TypeDefs[name]
-
-
1
elsif name.is_a?(DataConverter)
-
1
(type_map || TypeDefs)[name] = Type::Mapped.new(name)
-
else
-
raise TypeError, "unable to resolve type '#{name}'"
-
end
-
end
-
-
# List of type definitions
-
1
TypeDefs.merge!({
-
# The C void type; only useful for function return types
-
:void => Type::VOID,
-
-
# C boolean type
-
:bool => Type::BOOL,
-
-
# C nul-terminated string
-
:string => Type::STRING,
-
-
# C signed char
-
:char => Type::CHAR,
-
# C unsigned char
-
:uchar => Type::UCHAR,
-
-
# C signed short
-
:short => Type::SHORT,
-
# C unsigned short
-
:ushort => Type::USHORT,
-
-
# C signed int
-
:int => Type::INT,
-
# C unsigned int
-
:uint => Type::UINT,
-
-
# C signed long
-
:long => Type::LONG,
-
-
# C unsigned long
-
:ulong => Type::ULONG,
-
-
# C signed long long integer
-
:long_long => Type::LONG_LONG,
-
-
# C unsigned long long integer
-
:ulong_long => Type::ULONG_LONG,
-
-
# C single precision float
-
:float => Type::FLOAT,
-
-
# C double precision float
-
:double => Type::DOUBLE,
-
-
# C long double
-
:long_double => Type::LONGDOUBLE,
-
-
# Native memory address
-
:pointer => Type::POINTER,
-
-
# 8 bit signed integer
-
:int8 => Type::INT8,
-
# 8 bit unsigned integer
-
:uint8 => Type::UINT8,
-
-
# 16 bit signed integer
-
:int16 => Type::INT16,
-
# 16 bit unsigned integer
-
:uint16 => Type::UINT16,
-
-
# 32 bit signed integer
-
:int32 => Type::INT32,
-
# 32 bit unsigned integer
-
:uint32 => Type::UINT32,
-
-
# 64 bit signed integer
-
:int64 => Type::INT64,
-
# 64 bit unsigned integer
-
:uint64 => Type::UINT64,
-
-
:buffer_in => Type::BUFFER_IN,
-
:buffer_out => Type::BUFFER_OUT,
-
:buffer_inout => Type::BUFFER_INOUT,
-
-
# Used in function prototypes to indicate the arguments are variadic
-
:varargs => Type::VARARGS,
-
})
-
-
# This will convert a pointer to a Ruby string (just like `:string`), but
-
# also allow to work with the pointer itself. This is useful when you want
-
# a Ruby string already containing a copy of the data, but also the pointer
-
# to the data for you to do something with it, like freeing it, in case the
-
# library handed the memory off to the caller (Ruby-FFI).
-
#
-
# It's {typedef}'d as +:strptr+.
-
1
class StrPtrConverter
-
1
extend DataConverter
-
1
native_type Type::POINTER
-
-
# @param [Pointer] val
-
# @param ctx not used
-
# @return [Array(String, Pointer)]
-
# Returns a [ String, Pointer ] tuple so the C memory for the string can be freed
-
1
def self.from_native(val, ctx)
-
[ val.null? ? nil : val.get_string(0), val ]
-
end
-
end
-
-
1
typedef(StrPtrConverter, :strptr)
-
-
# @param type +type+ is an instance of class accepted by {FFI.find_type}
-
# @return [Numeric]
-
# Get +type+ size, in bytes.
-
1
def self.type_size(type)
-
find_type(type).size
-
end
-
-
# Load all the platform dependent types
-
begin
-
1
File.open(File.join(Platform::CONF_DIR, 'types.conf'), "r") do |f|
-
1
prefix = "rbx.platform.typedef."
-
1
f.each_line { |line|
-
132
if line.index(prefix) == 0
-
132
new_type, orig_type = line.chomp.slice(prefix.length..-1).split(/\s*=\s*/)
-
132
typedef(orig_type.to_sym, new_type.to_sym)
-
end
-
}
-
end
-
1
typedef :pointer, :caddr_t
-
rescue Errno::ENOENT
-
end
-
end
-
#
-
# Copyright (C) 2009 Andrea Fazzi <andrea.fazzi@alcacoop.it>
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
#
-
-
1
require 'ffi/struct'
-
-
1
module FFI
-
-
1
class Union < FFI::Struct
-
1
def self.builder
-
b = StructLayoutBuilder.new
-
b.union = true
-
b
-
end
-
end
-
end
-
#
-
# Copyright (C) 2008, 2009 Wayne Meissner
-
# Copyright (C) 2009 Luc Heinrich
-
#
-
# This file is part of ruby-ffi.
-
#
-
# All rights reserved.
-
#
-
# Redistribution and use in source and binary forms, with or without
-
# modification, are permitted provided that the following conditions are met:
-
#
-
# * Redistributions of source code must retain the above copyright notice, this
-
# list of conditions and the following disclaimer.
-
# * Redistributions in binary form must reproduce the above copyright notice
-
# this list of conditions and the following disclaimer in the documentation
-
# and/or other materials provided with the distribution.
-
# * Neither the name of the Ruby FFI project nor the names of its contributors
-
# may be used to endorse or promote products derived from this software
-
# without specific prior written permission.
-
#
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
#
-
-
1
module FFI
-
1
class VariadicInvoker
-
1
def init(arg_types, type_map)
-
@fixed = Array.new
-
@type_map = type_map
-
arg_types.each_with_index do |type, i|
-
@fixed << type unless type == Type::VARARGS
-
end
-
end
-
-
-
1
def call(*args, &block)
-
param_types = Array.new(@fixed)
-
param_values = Array.new
-
@fixed.each_with_index do |t, i|
-
param_values << args[i]
-
end
-
i = @fixed.length
-
while i < args.length
-
param_types << FFI.find_type(args[i], @type_map)
-
param_values << args[i + 1]
-
i += 2
-
end
-
invoke(param_types, param_values, &block)
-
end
-
-
#
-
# Attach the invoker to module +mod+ as +mname+
-
#
-
1
def attach(mod, mname)
-
invoker = self
-
params = "*args"
-
call = "call"
-
mod.module_eval <<-code
-
@@#{mname} = invoker
-
def self.#{mname}(#{params})
-
@@#{mname}.#{call}(#{params})
-
end
-
def #{mname}(#{params})
-
@@#{mname}.#{call}(#{params})
-
end
-
code
-
invoker
-
end
-
end
-
end
-
1
module FFI
-
1
VERSION = '1.13.1'
-
end
-
1
require "optparse"
-
1
require "thread"
-
1
require "mutex_m"
-
1
require "minitest/parallel"
-
1
require "stringio"
-
-
##
-
# :include: README.rdoc
-
-
1
module Minitest
-
1
VERSION = "5.14.1" # :nodoc:
-
1
ENCS = "".respond_to? :encoding # :nodoc:
-
-
1
@@installed_at_exit ||= false
-
1
@@after_run = []
-
1
@extensions = []
-
-
2
mc = (class << self; self; end)
-
-
##
-
# Parallel test executor
-
-
1
mc.send :attr_accessor, :parallel_executor
-
-
1
warn "DEPRECATED: use MT_CPU instead of N for parallel test runs" if ENV["N"]
-
1
n_threads = (ENV["MT_CPU"] || ENV["N"] || 2).to_i
-
1
self.parallel_executor = Parallel::Executor.new n_threads
-
-
##
-
# Filter object for backtraces.
-
-
1
mc.send :attr_accessor, :backtrace_filter
-
-
##
-
# Reporter object to be used for all runs.
-
#
-
# NOTE: This accessor is only available during setup, not during runs.
-
-
1
mc.send :attr_accessor, :reporter
-
-
##
-
# Names of known extension plugins.
-
-
1
mc.send :attr_accessor, :extensions
-
-
##
-
# The signal to use for dumping information to STDERR. Defaults to "INFO".
-
-
1
mc.send :attr_accessor, :info_signal
-
1
self.info_signal = "INFO"
-
-
##
-
# Registers Minitest to run at process exit
-
-
1
def self.autorun
-
at_exit {
-
1
next if $! and not ($!.kind_of? SystemExit and $!.success?)
-
-
1
exit_code = nil
-
-
1
pid = Process.pid
-
1
at_exit {
-
1
next if Process.pid != pid
-
1
@@after_run.reverse_each(&:call)
-
1
exit exit_code || false
-
}
-
-
1
exit_code = Minitest.run ARGV
-
1
} unless @@installed_at_exit
-
1
@@installed_at_exit = true
-
end
-
-
##
-
# A simple hook allowing you to run a block of code after everything
-
# is done running. Eg:
-
#
-
# Minitest.after_run { p $debugging_info }
-
-
1
def self.after_run &block
-
@@after_run << block
-
end
-
-
1
def self.init_plugins options # :nodoc:
-
1
self.extensions.each do |name|
-
1
msg = "plugin_#{name}_init"
-
1
send msg, options if self.respond_to? msg
-
end
-
end
-
-
1
def self.load_plugins # :nodoc:
-
1
return unless self.extensions.empty?
-
-
1
seen = {}
-
-
1
require "rubygems" unless defined? Gem
-
-
1
Gem.find_files("minitest/*_plugin.rb").each do |plugin_path|
-
2
name = File.basename plugin_path, "_plugin.rb"
-
-
2
next if seen[name]
-
1
seen[name] = true
-
-
1
require plugin_path
-
1
self.extensions << name
-
end
-
end
-
-
##
-
# This is the top-level run method. Everything starts from here. It
-
# tells each Runnable sub-class to run, and each of those are
-
# responsible for doing whatever they do.
-
#
-
# The overall structure of a run looks like this:
-
#
-
# Minitest.autorun
-
# Minitest.run(args)
-
# Minitest.__run(reporter, options)
-
# Runnable.runnables.each
-
# runnable.run(reporter, options)
-
# self.runnable_methods.each
-
# self.run_one_method(self, runnable_method, reporter)
-
# Minitest.run_one_method(klass, runnable_method)
-
# klass.new(runnable_method).run
-
-
1
def self.run args = []
-
1
self.load_plugins unless args.delete("--no-plugins") || ENV["MT_NO_PLUGINS"]
-
-
1
options = process_args args
-
-
1
reporter = CompositeReporter.new
-
1
reporter << SummaryReporter.new(options[:io], options)
-
1
reporter << ProgressReporter.new(options[:io], options)
-
-
1
self.reporter = reporter # this makes it available to plugins
-
1
self.init_plugins options
-
1
self.reporter = nil # runnables shouldn't depend on the reporter, ever
-
-
1
self.parallel_executor.start if parallel_executor.respond_to?(:start)
-
1
reporter.start
-
begin
-
1
__run reporter, options
-
rescue Interrupt
-
warn "Interrupted. Exiting..."
-
end
-
1
self.parallel_executor.shutdown
-
1
reporter.report
-
-
1
reporter.passed?
-
end
-
-
##
-
# Internal run method. Responsible for telling all Runnable
-
# sub-classes to run.
-
-
1
def self.__run reporter, options
-
8
suites = Runnable.runnables.reject { |s| s.runnable_methods.empty? }.shuffle
-
3
parallel, serial = suites.partition { |s| s.test_order == :parallel }
-
-
# If we run the parallel tests before the serial tests, the parallel tests
-
# could run in parallel with the serial tests. This would be bad because
-
# the serial tests won't lock around Reporter#record. Run the serial tests
-
# first, so that after they complete, the parallel tests will lock when
-
# recording results.
-
3
serial.map { |suite| suite.run reporter, options } +
-
parallel.map { |suite| suite.run reporter, options }
-
end
-
-
1
def self.process_args args = [] # :nodoc:
-
options = {
-
1
:io => $stdout,
-
}
-
1
orig_args = args.dup
-
-
1
OptionParser.new do |opts|
-
1
opts.banner = "minitest options:"
-
1
opts.version = Minitest::VERSION
-
-
1
opts.on "-h", "--help", "Display this help." do
-
puts opts
-
exit
-
end
-
-
1
opts.on "--no-plugins", "Bypass minitest plugin auto-loading (or set $MT_NO_PLUGINS)."
-
-
1
desc = "Sets random seed. Also via env. Eg: SEED=n rake"
-
1
opts.on "-s", "--seed SEED", Integer, desc do |m|
-
options[:seed] = m.to_i
-
end
-
-
1
opts.on "-v", "--verbose", "Verbose. Show progress processing files." do
-
options[:verbose] = true
-
end
-
-
1
opts.on "-n", "--name PATTERN", "Filter run on /regexp/ or string." do |a|
-
options[:filter] = a
-
end
-
-
1
opts.on "-e", "--exclude PATTERN", "Exclude /regexp/ or string from run." do |a|
-
options[:exclude] = a
-
end
-
-
1
unless extensions.empty?
-
1
opts.separator ""
-
1
opts.separator "Known extensions: #{extensions.join(", ")}"
-
-
1
extensions.each do |meth|
-
1
msg = "plugin_#{meth}_options"
-
1
send msg, opts, options if self.respond_to?(msg)
-
end
-
end
-
-
begin
-
1
opts.parse! args
-
rescue OptionParser::InvalidOption => e
-
puts
-
puts e
-
puts
-
puts opts
-
exit 1
-
end
-
-
1
orig_args -= args
-
end
-
-
1
unless options[:seed] then
-
1
srand
-
1
options[:seed] = (ENV["SEED"] || srand).to_i % 0xFFFF
-
1
orig_args << "--seed" << options[:seed].to_s
-
end
-
-
1
srand options[:seed]
-
-
1
options[:args] = orig_args.map { |s|
-
2
s =~ /[\s|&<>$()]/ ? s.inspect : s
-
}.join " "
-
-
1
options
-
end
-
-
1
def self.filter_backtrace bt # :nodoc:
-
result = backtrace_filter.filter bt
-
result = bt.dup if result.empty?
-
result
-
end
-
-
##
-
# Represents anything "runnable", like Test, Spec, Benchmark, or
-
# whatever you can dream up.
-
#
-
# Subclasses of this are automatically registered and available in
-
# Runnable.runnables.
-
-
1
class Runnable
-
##
-
# Number of assertions executed in this run.
-
-
1
attr_accessor :assertions
-
-
##
-
# An assertion raised during the run, if any.
-
-
1
attr_accessor :failures
-
-
##
-
# The time it took to run.
-
-
1
attr_accessor :time
-
-
1
def time_it # :nodoc:
-
3
t0 = Minitest.clock_time
-
-
3
yield
-
ensure
-
3
self.time = Minitest.clock_time - t0
-
end
-
-
##
-
# Name of the run.
-
-
1
def name
-
9
@NAME
-
end
-
-
##
-
# Set the name of the run.
-
-
1
def name= o
-
6
@NAME = o
-
end
-
-
##
-
# Returns all instance methods matching the pattern +re+.
-
-
1
def self.methods_matching re
-
9
public_instance_methods(true).grep(re).map(&:to_s)
-
end
-
-
1
def self.reset # :nodoc:
-
1
@@runnables = []
-
end
-
-
1
reset
-
-
##
-
# Responsible for running all runnable methods in a given class,
-
# each in its own instance. Each instance is passed to the
-
# reporter to record.
-
-
1
def self.run reporter, options = {}
-
2
filter = options[:filter] || "/./"
-
2
filter = Regexp.new $1 if filter.is_a?(String) && filter =~ %r%/(.*)/%
-
-
2
filtered_methods = self.runnable_methods.find_all { |m|
-
3
filter === m || filter === "#{self}##{m}"
-
}
-
-
2
exclude = options[:exclude]
-
2
exclude = Regexp.new $1 if exclude =~ %r%/(.*)/%
-
-
2
filtered_methods.delete_if { |m|
-
3
exclude === m || exclude === "#{self}##{m}"
-
}
-
-
2
return if filtered_methods.empty?
-
-
2
with_info_handler reporter do
-
2
filtered_methods.each do |method_name|
-
3
run_one_method self, method_name, reporter
-
end
-
end
-
end
-
-
##
-
# Runs a single method and has the reporter record the result.
-
# This was considered internal API but is factored out of run so
-
# that subclasses can specialize the running of an individual
-
# test. See Minitest::ParallelTest::ClassMethods for an example.
-
-
1
def self.run_one_method klass, method_name, reporter
-
3
reporter.prerecord klass, method_name
-
3
reporter.record Minitest.run_one_method(klass, method_name)
-
end
-
-
1
def self.with_info_handler reporter, &block # :nodoc:
-
2
handler = lambda do
-
unless reporter.passed? then
-
warn "Current results:"
-
warn ""
-
warn reporter.reporters.first
-
warn ""
-
end
-
end
-
-
2
on_signal ::Minitest.info_signal, handler, &block
-
end
-
-
1
SIGNALS = Signal.list # :nodoc:
-
-
1
def self.on_signal name, action # :nodoc:
-
5
supported = SIGNALS[name]
-
-
old_trap = trap name do
-
old_trap.call if old_trap.respond_to? :call
-
action.call
-
5
end if supported
-
-
5
yield
-
ensure
-
5
trap name, old_trap if supported
-
end
-
-
##
-
# Each subclass of Runnable is responsible for overriding this
-
# method to return all runnable methods. See #methods_matching.
-
-
1
def self.runnable_methods
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Returns all subclasses of Runnable.
-
-
1
def self.runnables
-
8
@@runnables
-
end
-
-
1
@@marshal_dump_warned = false
-
-
1
def marshal_dump # :nodoc:
-
unless @@marshal_dump_warned then
-
warn ["Minitest::Runnable#marshal_dump is deprecated.",
-
"You might be violating internals. From", caller.first].join " "
-
@@marshal_dump_warned = true
-
end
-
-
[self.name, self.failures, self.assertions, self.time]
-
end
-
-
1
def marshal_load ary # :nodoc:
-
self.name, self.failures, self.assertions, self.time = ary
-
end
-
-
1
def failure # :nodoc:
-
9
self.failures.first
-
end
-
-
1
def initialize name # :nodoc:
-
6
self.name = name
-
6
self.failures = []
-
6
self.assertions = 0
-
end
-
-
##
-
# Runs a single method. Needs to return self.
-
-
1
def run
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Did this run pass?
-
#
-
# Note: skipped runs are not considered passing, but they don't
-
# cause the process to exit non-zero.
-
-
1
def passed?
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Returns a single character string to print based on the result
-
# of the run. One of <tt>"."</tt>, <tt>"F"</tt>,
-
# <tt>"E"</tt> or <tt>"S"</tt>.
-
-
1
def result_code
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Was this run skipped? See #passed? for more information.
-
-
1
def skipped?
-
raise NotImplementedError, "subclass responsibility"
-
end
-
end
-
-
##
-
# Shared code for anything that can get passed to a Reporter. See
-
# Minitest::Test & Minitest::Result.
-
-
1
module Reportable
-
##
-
# Did this run pass?
-
#
-
# Note: skipped runs are not considered passing, but they don't
-
# cause the process to exit non-zero.
-
-
1
def passed?
-
3
not self.failure
-
end
-
-
##
-
# The location identifier of this test. Depends on a method
-
# existing called class_name.
-
-
1
def location
-
loc = " [#{self.failure.location}]" unless passed? or error?
-
"#{self.class_name}##{self.name}#{loc}"
-
end
-
-
1
def class_name # :nodoc:
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Returns ".", "F", or "E" based on the result of the run.
-
-
1
def result_code
-
3
self.failure and self.failure.result_code or "."
-
end
-
-
##
-
# Was this run skipped?
-
-
1
def skipped?
-
3
self.failure and Skip === self.failure
-
end
-
-
##
-
# Did this run error?
-
-
1
def error?
-
self.failures.any? { |f| UnexpectedError === f }
-
end
-
end
-
-
##
-
# This represents a test result in a clean way that can be
-
# marshalled over a wire. Tests can do anything they want to the
-
# test instance and can create conditions that cause Marshal.dump to
-
# blow up. By using Result.from(a_test) you can be reasonably sure
-
# that the test result can be marshalled.
-
-
1
class Result < Runnable
-
1
include Minitest::Reportable
-
-
1
undef_method :marshal_dump
-
1
undef_method :marshal_load
-
-
##
-
# The class name of the test result.
-
-
1
attr_accessor :klass
-
-
##
-
# The location of the test method.
-
-
1
attr_accessor :source_location
-
-
##
-
# Create a new test result from a Runnable instance.
-
-
1
def self.from runnable
-
3
o = runnable
-
-
3
r = self.new o.name
-
3
r.klass = o.class.name
-
3
r.assertions = o.assertions
-
3
r.failures = o.failures.dup
-
3
r.time = o.time
-
-
3
r.source_location = o.method(o.name).source_location rescue ["unknown", -1]
-
-
3
r
-
end
-
-
1
def class_name # :nodoc:
-
self.klass # for Minitest::Reportable
-
end
-
-
1
def to_s # :nodoc:
-
return location if passed? and not skipped?
-
-
failures.map { |failure|
-
"#{failure.result_label}:\n#{self.location}:\n#{failure.message}\n"
-
}.join "\n"
-
end
-
end
-
-
##
-
# Defines the API for Reporters. Subclass this and override whatever
-
# you want. Go nuts.
-
-
1
class AbstractReporter
-
1
include Mutex_m
-
-
##
-
# Starts reporting on the run.
-
-
1
def start
-
end
-
-
##
-
# About to start running a test. This allows a reporter to show
-
# that it is starting or that we are in the middle of a test run.
-
-
1
def prerecord klass, name
-
end
-
-
##
-
# Output and record the result of the test. Call
-
# {result#result_code}[rdoc-ref:Runnable#result_code] to get the
-
# result character string. Stores the result of the run if the run
-
# did not pass.
-
-
1
def record result
-
end
-
-
##
-
# Outputs the summary of the run.
-
-
1
def report
-
end
-
-
##
-
# Did this run pass?
-
-
1
def passed?
-
1
true
-
end
-
end
-
-
1
class Reporter < AbstractReporter # :nodoc:
-
##
-
# The IO used to report.
-
-
1
attr_accessor :io
-
-
##
-
# Command-line options for this run.
-
-
1
attr_accessor :options
-
-
1
def initialize io = $stdout, options = {} # :nodoc:
-
2
super()
-
2
self.io = io
-
2
self.options = options
-
end
-
end
-
-
##
-
# A very simple reporter that prints the "dots" during the run.
-
#
-
# This is added to the top-level CompositeReporter at the start of
-
# the run. If you want to change the output of minitest via a
-
# plugin, pull this out of the composite and replace it with your
-
# own.
-
-
1
class ProgressReporter < Reporter
-
1
def prerecord klass, name #:nodoc:
-
3
if options[:verbose] then
-
io.print "%s#%s = " % [klass.name, name]
-
io.flush
-
end
-
end
-
-
1
def record result # :nodoc:
-
3
io.print "%.2f s = " % [result.time] if options[:verbose]
-
3
io.print result.result_code
-
3
io.puts if options[:verbose]
-
end
-
end
-
-
##
-
# A reporter that gathers statistics about a test run. Does not do
-
# any IO because meant to be used as a parent class for a reporter
-
# that does.
-
#
-
# If you want to create an entirely different type of output (eg,
-
# CI, HTML, etc), this is the place to start.
-
#
-
# Example:
-
#
-
# class JenkinsCIReporter < StatisticsReporter
-
# def report
-
# super # Needed to calculate some statistics
-
#
-
# print "<testsuite "
-
# print "tests='#{count}' "
-
# print "failures='#{failures}' "
-
# # Remaining XML...
-
# end
-
# end
-
-
1
class StatisticsReporter < Reporter
-
##
-
# Total number of assertions.
-
-
1
attr_accessor :assertions
-
-
##
-
# Total number of test cases.
-
-
1
attr_accessor :count
-
-
##
-
# An +Array+ of test cases that failed or were skipped.
-
-
1
attr_accessor :results
-
-
##
-
# Time the test run started. If available, the monotonic clock is
-
# used and this is a +Float+, otherwise it's an instance of
-
# +Time+.
-
-
1
attr_accessor :start_time
-
-
##
-
# Test run time. If available, the monotonic clock is used and
-
# this is a +Float+, otherwise it's an instance of +Time+.
-
-
1
attr_accessor :total_time
-
-
##
-
# Total number of tests that failed.
-
-
1
attr_accessor :failures
-
-
##
-
# Total number of tests that erred.
-
-
1
attr_accessor :errors
-
-
##
-
# Total number of tests that where skipped.
-
-
1
attr_accessor :skips
-
-
1
def initialize io = $stdout, options = {} # :nodoc:
-
1
super
-
-
1
self.assertions = 0
-
1
self.count = 0
-
1
self.results = []
-
1
self.start_time = nil
-
1
self.total_time = nil
-
1
self.failures = nil
-
1
self.errors = nil
-
1
self.skips = nil
-
end
-
-
1
def passed? # :nodoc:
-
1
results.all?(&:skipped?)
-
end
-
-
1
def start # :nodoc:
-
1
self.start_time = Minitest.clock_time
-
end
-
-
1
def record result # :nodoc:
-
3
self.count += 1
-
3
self.assertions += result.assertions
-
-
3
results << result if not result.passed? or result.skipped?
-
end
-
-
##
-
# Report on the tracked statistics.
-
-
1
def report
-
1
aggregate = results.group_by { |r| r.failure.class }
-
1
aggregate.default = [] # dumb. group_by should provide this
-
-
1
self.total_time = Minitest.clock_time - start_time
-
1
self.failures = aggregate[Assertion].size
-
1
self.errors = aggregate[UnexpectedError].size
-
1
self.skips = aggregate[Skip].size
-
end
-
end
-
-
##
-
# A reporter that prints the header, summary, and failure details at
-
# the end of the run.
-
#
-
# This is added to the top-level CompositeReporter at the start of
-
# the run. If you want to change the output of minitest via a
-
# plugin, pull this out of the composite and replace it with your
-
# own.
-
-
1
class SummaryReporter < StatisticsReporter
-
# :stopdoc:
-
1
attr_accessor :sync
-
1
attr_accessor :old_sync
-
# :startdoc:
-
-
1
def start # :nodoc:
-
1
super
-
-
1
io.puts "Run options: #{options[:args]}"
-
1
io.puts
-
1
io.puts "# Running:"
-
1
io.puts
-
-
1
self.sync = io.respond_to? :"sync=" # stupid emacs
-
1
self.old_sync, io.sync = io.sync, true if self.sync
-
end
-
-
1
def report # :nodoc:
-
1
super
-
-
1
io.sync = self.old_sync
-
-
1
io.puts unless options[:verbose] # finish the dots
-
1
io.puts
-
1
io.puts statistics
-
1
aggregated_results io
-
1
io.puts summary
-
end
-
-
1
def statistics # :nodoc:
-
1
"Finished in %.6fs, %.4f runs/s, %.4f assertions/s." %
-
[total_time, count / total_time, assertions / total_time]
-
end
-
-
1
def aggregated_results io # :nodoc:
-
1
filtered_results = results.dup
-
1
filtered_results.reject!(&:skipped?) unless options[:verbose]
-
-
1
filtered_results.each_with_index { |result, i|
-
io.puts "\n%3d) %s" % [i+1, result]
-
}
-
1
io.puts
-
1
io
-
end
-
-
1
def to_s # :nodoc:
-
aggregated_results(StringIO.new(binary_string)).string
-
end
-
-
1
def summary # :nodoc:
-
1
extra = ""
-
-
extra = "\n\nYou have skipped tests. Run with --verbose for details." if
-
1
results.any?(&:skipped?) unless options[:verbose] or ENV["MT_NO_SKIP_MSG"]
-
-
1
"%d runs, %d assertions, %d failures, %d errors, %d skips%s" %
-
[count, assertions, failures, errors, skips, extra]
-
end
-
-
1
private
-
-
1
if '<3'.respond_to? :b
-
1
def binary_string; ''.b; end
-
else
-
def binary_string; ''.force_encoding(Encoding::ASCII_8BIT); end
-
end
-
end
-
-
##
-
# Dispatch to multiple reporters as one.
-
-
1
class CompositeReporter < AbstractReporter
-
##
-
# The list of reporters to dispatch to.
-
-
1
attr_accessor :reporters
-
-
1
def initialize *reporters # :nodoc:
-
1
super()
-
1
self.reporters = reporters
-
end
-
-
1
def io # :nodoc:
-
reporters.first.io
-
end
-
-
##
-
# Add another reporter to the mix.
-
-
1
def << reporter
-
2
self.reporters << reporter
-
end
-
-
1
def passed? # :nodoc:
-
1
self.reporters.all?(&:passed?)
-
end
-
-
1
def start # :nodoc:
-
1
self.reporters.each(&:start)
-
end
-
-
1
def prerecord klass, name # :nodoc:
-
3
self.reporters.each do |reporter|
-
# TODO: remove conditional for minitest 6
-
6
reporter.prerecord klass, name if reporter.respond_to? :prerecord
-
end
-
end
-
-
1
def record result # :nodoc:
-
3
self.reporters.each do |reporter|
-
6
reporter.record result
-
end
-
end
-
-
1
def report # :nodoc:
-
1
self.reporters.each(&:report)
-
end
-
end
-
-
##
-
# Represents run failures.
-
-
1
class Assertion < Exception
-
1
def error # :nodoc:
-
self
-
end
-
-
##
-
# Where was this run before an assertion was raised?
-
-
1
def location
-
last_before_assertion = ""
-
self.backtrace.reverse_each do |s|
-
break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
-
last_before_assertion = s
-
end
-
last_before_assertion.sub(/:in .*$/, "")
-
end
-
-
1
def result_code # :nodoc:
-
result_label[0, 1]
-
end
-
-
1
def result_label # :nodoc:
-
"Failure"
-
end
-
end
-
-
##
-
# Assertion raised when skipping a run.
-
-
1
class Skip < Assertion
-
1
def result_label # :nodoc:
-
"Skipped"
-
end
-
end
-
-
##
-
# Assertion wrapping an unexpected error that was raised during a run.
-
-
1
class UnexpectedError < Assertion
-
# TODO: figure out how to use `cause` instead
-
1
attr_accessor :error # :nodoc:
-
-
1
def initialize error # :nodoc:
-
super "Unexpected exception"
-
self.error = error
-
end
-
-
1
def backtrace # :nodoc:
-
self.error.backtrace
-
end
-
-
1
def message # :nodoc:
-
bt = Minitest.filter_backtrace(self.backtrace).join "\n "
-
"#{self.error.class}: #{self.error.message}\n #{bt}"
-
end
-
-
1
def result_label # :nodoc:
-
"Error"
-
end
-
end
-
-
##
-
# Provides a simple set of guards that you can use in your tests
-
# to skip execution if it is not applicable. These methods are
-
# mixed into Test as both instance and class methods so you
-
# can use them inside or outside of the test methods.
-
#
-
# def test_something_for_mri
-
# skip "bug 1234" if jruby?
-
# # ...
-
# end
-
#
-
# if windows? then
-
# # ... lots of test methods ...
-
# end
-
-
1
module Guard
-
-
##
-
# Is this running on jruby?
-
-
1
def jruby? platform = RUBY_PLATFORM
-
"java" == platform
-
end
-
-
##
-
# Is this running on maglev?
-
-
1
def maglev? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
warn "DEPRECATED: `maglev?` called from #{where}. This will fail in Minitest 6."
-
"maglev" == platform
-
end
-
-
##
-
# Is this running on mri?
-
-
1
def mri? platform = RUBY_DESCRIPTION
-
/^ruby/ =~ platform
-
end
-
-
##
-
# Is this running on macOS?
-
-
1
def osx? platform = RUBY_PLATFORM
-
/darwin/ =~ platform
-
end
-
-
##
-
# Is this running on rubinius?
-
-
1
def rubinius? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
warn "DEPRECATED: `rubinius?` called from #{where}. This will fail in Minitest 6."
-
"rbx" == platform
-
end
-
-
##
-
# Is this running on windows?
-
-
1
def windows? platform = RUBY_PLATFORM
-
/mswin|mingw/ =~ platform
-
end
-
end
-
-
##
-
# The standard backtrace filter for minitest.
-
#
-
# See Minitest.backtrace_filter=.
-
-
1
class BacktraceFilter
-
-
1
MT_RE = %r%lib/minitest% #:nodoc:
-
-
##
-
# Filter +bt+ to something useful. Returns the whole thing if
-
# $DEBUG (ruby) or $MT_DEBUG (env).
-
-
1
def filter bt
-
return ["No backtrace"] unless bt
-
-
return bt.dup if $DEBUG || ENV["MT_DEBUG"]
-
-
new_bt = bt.take_while { |line| line !~ MT_RE }
-
new_bt = bt.select { |line| line !~ MT_RE } if new_bt.empty?
-
new_bt = bt.dup if new_bt.empty?
-
-
new_bt
-
end
-
end
-
-
1
self.backtrace_filter = BacktraceFilter.new
-
-
1
def self.run_one_method klass, method_name # :nodoc:
-
3
result = klass.new(method_name).run
-
3
raise "#{klass}#run _must_ return a Result" unless Result === result
-
3
result
-
end
-
-
# :stopdoc:
-
-
1
if defined? Process::CLOCK_MONOTONIC # :nodoc:
-
1
def self.clock_time
-
11
Process.clock_gettime Process::CLOCK_MONOTONIC
-
end
-
else
-
def self.clock_time
-
Time.now
-
end
-
end
-
-
1
class Runnable # re-open
-
1
def self.inherited klass # :nodoc:
-
7
self.runnables << klass
-
7
super
-
end
-
end
-
-
# :startdoc:
-
end
-
-
1
require "minitest/test"
-
# encoding: UTF-8
-
-
1
require "rbconfig"
-
1
require "tempfile"
-
1
require "stringio"
-
-
1
module Minitest
-
##
-
# Minitest Assertions. All assertion methods accept a +msg+ which is
-
# printed if the assertion fails.
-
#
-
# Protocol: Nearly everything here boils up to +assert+, which
-
# expects to be able to increment an instance accessor named
-
# +assertions+. This is not provided by Assertions and must be
-
# provided by the thing including Assertions. See Minitest::Runnable
-
# for an example.
-
-
1
module Assertions
-
1
UNDEFINED = Object.new # :nodoc:
-
-
1
def UNDEFINED.inspect # :nodoc:
-
"UNDEFINED" # again with the rdoc bugs... :(
-
end
-
-
##
-
# Returns the diff command to use in #diff. Tries to intelligently
-
# figure out what diff to use.
-
-
1
def self.diff
-
return @diff if defined? @diff
-
-
@diff = if (RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ &&
-
system("diff.exe", __FILE__, __FILE__)) then
-
"diff.exe -u"
-
elsif system("gdiff", __FILE__, __FILE__)
-
"gdiff -u" # solaris and kin suck
-
elsif system("diff", __FILE__, __FILE__)
-
"diff -u"
-
else
-
nil
-
end
-
end
-
-
##
-
# Set the diff command to use in #diff.
-
-
1
def self.diff= o
-
@diff = o
-
end
-
-
##
-
# Returns a diff between +exp+ and +act+. If there is no known
-
# diff command or if it doesn't make sense to diff the output
-
# (single line, short output), then it simply returns a basic
-
# comparison between the two.
-
#
-
# See +things_to_diff+ for more info.
-
-
1
def diff exp, act
-
result = nil
-
-
expect, butwas = things_to_diff(exp, act)
-
-
return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" unless
-
expect
-
-
Tempfile.open("expect") do |a|
-
a.puts expect
-
a.flush
-
-
Tempfile.open("butwas") do |b|
-
b.puts butwas
-
b.flush
-
-
result = `#{Minitest::Assertions.diff} #{a.path} #{b.path}`
-
result.sub!(/^\-\-\- .+/, "--- expected")
-
result.sub!(/^\+\+\+ .+/, "+++ actual")
-
-
if result.empty? then
-
klass = exp.class
-
result = [
-
"No visible difference in the #{klass}#inspect output.\n",
-
"You should look at the implementation of #== on ",
-
"#{klass} or its members.\n",
-
expect,
-
].join
-
end
-
end
-
end
-
-
result
-
end
-
-
##
-
# Returns things to diff [expect, butwas], or [nil, nil] if nothing to diff.
-
#
-
# Criterion:
-
#
-
# 1. Strings include newlines or escaped newlines, but not both.
-
# 2. or: String lengths are > 30 characters.
-
# 3. or: Strings are equal to each other (but maybe different encodings?).
-
# 4. and: we found a diff executable.
-
-
1
def things_to_diff exp, act
-
expect = mu_pp_for_diff exp
-
butwas = mu_pp_for_diff act
-
-
e1, e2 = expect.include?("\n"), expect.include?("\\n")
-
b1, b2 = butwas.include?("\n"), butwas.include?("\\n")
-
-
need_to_diff =
-
(e1 ^ e2 ||
-
b1 ^ b2 ||
-
expect.size > 30 ||
-
butwas.size > 30 ||
-
expect == butwas) &&
-
Minitest::Assertions.diff
-
-
need_to_diff && [expect, butwas]
-
end
-
-
##
-
# This returns a human-readable version of +obj+. By default
-
# #inspect is called. You can override this to use #pretty_inspect
-
# if you want.
-
#
-
# See Minitest::Test.make_my_diffs_pretty!
-
-
1
def mu_pp obj
-
s = obj.inspect
-
-
if defined? Encoding then
-
s = s.encode Encoding.default_external
-
-
if String === obj && (obj.encoding != Encoding.default_external ||
-
!obj.valid_encoding?) then
-
enc = "# encoding: #{obj.encoding}"
-
val = "# valid: #{obj.valid_encoding?}"
-
s = "#{enc}\n#{val}\n#{s}"
-
end
-
end
-
-
s
-
end
-
-
##
-
# This returns a diff-able more human-readable version of +obj+.
-
# This differs from the regular mu_pp because it expands escaped
-
# newlines and makes hex-values (like object_ids) generic. This
-
# uses mu_pp to do the first pass and then cleans it up.
-
-
1
def mu_pp_for_diff obj
-
str = mu_pp obj
-
-
# both '\n' & '\\n' (_after_ mu_pp (aka inspect))
-
single = !!str.match(/(?<!\\|^)\\n/)
-
double = !!str.match(/(?<=\\|^)\\n/)
-
-
process =
-
if single ^ double then
-
if single then
-
lambda { |s| s == "\\n" ? "\n" : s } # unescape
-
else
-
lambda { |s| s == "\\\\n" ? "\\n\n" : s } # unescape a bit, add nls
-
end
-
else
-
:itself # leave it alone
-
end
-
-
str.
-
gsub(/\\?\\n/, &process).
-
gsub(/:0x[a-fA-F0-9]{4,}/m, ":0xXXXXXX") # anonymize hex values
-
end
-
-
##
-
# Fails unless +test+ is truthy.
-
-
1
def assert test, msg = nil
-
5
self.assertions += 1
-
5
unless test then
-
msg ||= "Expected #{mu_pp test} to be truthy."
-
msg = msg.call if Proc === msg
-
raise Minitest::Assertion, msg
-
end
-
5
true
-
end
-
-
1
def _synchronize # :nodoc:
-
yield
-
end
-
-
##
-
# Fails unless +obj+ is empty.
-
-
1
def assert_empty obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to be empty" }
-
assert_respond_to obj, :empty?
-
assert obj.empty?, msg
-
end
-
-
1
E = "" # :nodoc:
-
-
##
-
# Fails unless <tt>exp == act</tt> printing the difference between
-
# the two, if possible.
-
#
-
# If there is no visible difference but the assertion fails, you
-
# should suspect that your #== is buggy, or your inspect output is
-
# missing crucial details. For nicer structural diffing, set
-
# Minitest::Test.make_my_diffs_pretty!
-
#
-
# For floats use assert_in_delta.
-
#
-
# See also: Minitest::Assertions.diff
-
-
1
def assert_equal exp, act, msg = nil
-
1
msg = message(msg, E) { diff exp, act }
-
1
result = assert exp == act, msg
-
-
1
if nil == exp then
-
if Minitest::VERSION =~ /^6/ then
-
refute_nil exp, "Use assert_nil if expecting nil."
-
else
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
-
warn "DEPRECATED: Use assert_nil if expecting nil from #{where}. This will fail in Minitest 6."
-
end
-
end
-
-
1
result
-
end
-
-
##
-
# For comparing Floats. Fails unless +exp+ and +act+ are within +delta+
-
# of each other.
-
#
-
# assert_in_delta Math::PI, (22.0 / 7.0), 0.01
-
-
1
def assert_in_delta exp, act, delta = 0.001, msg = nil
-
n = (exp - act).abs
-
msg = message(msg) {
-
"Expected |#{exp} - #{act}| (#{n}) to be <= #{delta}"
-
}
-
assert delta >= n, msg
-
end
-
-
##
-
# For comparing Floats. Fails unless +exp+ and +act+ have a relative
-
# error less than +epsilon+.
-
-
1
def assert_in_epsilon exp, act, epsilon = 0.001, msg = nil
-
assert_in_delta exp, act, [exp.abs, act.abs].min * epsilon, msg
-
end
-
-
##
-
# Fails unless +collection+ includes +obj+.
-
-
1
def assert_includes collection, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(collection)} to include #{mu_pp(obj)}"
-
}
-
assert_respond_to collection, :include?
-
assert collection.include?(obj), msg
-
end
-
-
##
-
# Fails unless +obj+ is an instance of +cls+.
-
-
1
def assert_instance_of cls, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} to be an instance of #{cls}, not #{obj.class}"
-
}
-
-
assert obj.instance_of?(cls), msg
-
end
-
-
##
-
# Fails unless +obj+ is a kind of +cls+.
-
-
1
def assert_kind_of cls, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} to be a kind of #{cls}, not #{obj.class}" }
-
-
assert obj.kind_of?(cls), msg
-
end
-
-
##
-
# Fails unless +matcher+ <tt>=~</tt> +obj+.
-
-
1
def assert_match matcher, obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp matcher} to match #{mu_pp obj}" }
-
assert_respond_to matcher, :"=~"
-
matcher = Regexp.new Regexp.escape matcher if String === matcher
-
assert matcher =~ obj, msg
-
end
-
-
##
-
# Fails unless +obj+ is nil
-
-
1
def assert_nil obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to be nil" }
-
assert obj.nil?, msg
-
end
-
-
##
-
# For testing with binary operators. Eg:
-
#
-
# assert_operator 5, :<=, 4
-
-
1
def assert_operator o1, op, o2 = UNDEFINED, msg = nil
-
return assert_predicate o1, op, msg if UNDEFINED == o2
-
msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op} #{mu_pp(o2)}" }
-
assert o1.__send__(op, o2), msg
-
end
-
-
##
-
# Fails if stdout or stderr do not output the expected results.
-
# Pass in nil if you don't care about that streams output. Pass in
-
# "" if you require it to be silent. Pass in a regexp if you want
-
# to pattern match.
-
#
-
# assert_output(/hey/) { method_with_output }
-
#
-
# NOTE: this uses #capture_io, not #capture_subprocess_io.
-
#
-
# See also: #assert_silent
-
-
1
def assert_output stdout = nil, stderr = nil
-
flunk "assert_output requires a block to capture output." unless
-
block_given?
-
-
out, err = capture_io do
-
yield
-
end
-
-
err_msg = Regexp === stderr ? :assert_match : :assert_equal if stderr
-
out_msg = Regexp === stdout ? :assert_match : :assert_equal if stdout
-
-
y = send err_msg, stderr, err, "In stderr" if err_msg
-
x = send out_msg, stdout, out, "In stdout" if out_msg
-
-
(!stdout || x) && (!stderr || y)
-
rescue Assertion
-
raise
-
rescue => e
-
raise UnexpectedError, e
-
end
-
-
##
-
# Fails unless +path+ exists.
-
-
1
def assert_path_exists path, msg = nil
-
msg = message(msg) { "Expected path '#{path}' to exist" }
-
assert File.exist?(path), msg
-
end
-
-
##
-
# For testing with predicates. Eg:
-
#
-
# assert_predicate str, :empty?
-
#
-
# This is really meant for specs and is front-ended by assert_operator:
-
#
-
# str.must_be :empty?
-
-
1
def assert_predicate o1, op, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op}" }
-
assert o1.__send__(op), msg
-
end
-
-
##
-
# Fails unless the block raises one of +exp+. Returns the
-
# exception matched so you can check the message, attributes, etc.
-
#
-
# +exp+ takes an optional message on the end to help explain
-
# failures and defaults to StandardError if no exception class is
-
# passed. Eg:
-
#
-
# assert_raises(CustomError) { method_with_custom_error }
-
#
-
# With custom error message:
-
#
-
# assert_raises(CustomError, 'This should have raised CustomError') { method_with_custom_error }
-
#
-
# Using the returned object:
-
#
-
# error = assert_raises(CustomError) do
-
# raise CustomError, 'This is really bad'
-
# end
-
#
-
# assert_equal 'This is really bad', error.message
-
-
1
def assert_raises *exp
-
flunk "assert_raises requires a block to capture errors." unless
-
block_given?
-
-
msg = "#{exp.pop}.\n" if String === exp.last
-
exp << StandardError if exp.empty?
-
-
begin
-
yield
-
rescue *exp => e
-
pass # count assertion
-
return e
-
rescue Minitest::Assertion # incl Skip & UnexpectedError
-
# don't count assertion
-
raise
-
rescue SignalException, SystemExit
-
raise
-
rescue Exception => e
-
flunk proc {
-
exception_details(e, "#{msg}#{mu_pp(exp)} exception expected, not")
-
}
-
end
-
-
exp = exp.first if exp.size == 1
-
-
flunk "#{msg}#{mu_pp(exp)} expected but nothing was raised."
-
end
-
-
##
-
# Fails unless +obj+ responds to +meth+.
-
-
1
def assert_respond_to obj, meth, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}"
-
}
-
assert obj.respond_to?(meth), msg
-
end
-
-
##
-
# Fails unless +exp+ and +act+ are #equal?
-
-
1
def assert_same exp, act, msg = nil
-
msg = message(msg) {
-
data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
-
"Expected %s (oid=%d) to be the same as %s (oid=%d)" % data
-
}
-
assert exp.equal?(act), msg
-
end
-
-
##
-
# +send_ary+ is a receiver, message and arguments.
-
#
-
# Fails unless the call returns a true value
-
-
1
def assert_send send_ary, m = nil
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
warn "DEPRECATED: assert_send. From #{where}"
-
-
recv, msg, *args = send_ary
-
m = message(m) {
-
"Expected #{mu_pp(recv)}.#{msg}(*#{mu_pp(args)}) to return true" }
-
assert recv.__send__(msg, *args), m
-
end
-
-
##
-
# Fails if the block outputs anything to stderr or stdout.
-
#
-
# See also: #assert_output
-
-
1
def assert_silent
-
assert_output "", "" do
-
yield
-
end
-
end
-
-
##
-
# Fails unless the block throws +sym+
-
-
1
def assert_throws sym, msg = nil
-
default = "Expected #{mu_pp(sym)} to have been thrown"
-
caught = true
-
catch(sym) do
-
begin
-
yield
-
rescue ThreadError => e # wtf?!? 1.8 + threads == suck
-
default += ", not \:#{e.message[/uncaught throw \`(\w+?)\'/, 1]}"
-
rescue ArgumentError => e # 1.9 exception
-
raise e unless e.message.include?("uncaught throw")
-
default += ", not #{e.message.split(/ /).last}"
-
rescue NameError => e # 1.8 exception
-
raise e unless e.name == sym
-
default += ", not #{e.name.inspect}"
-
end
-
caught = false
-
end
-
-
assert caught, message(msg) { default }
-
rescue Assertion
-
raise
-
rescue => e
-
raise UnexpectedError, e
-
end
-
-
##
-
# Captures $stdout and $stderr into strings:
-
#
-
# out, err = capture_io do
-
# puts "Some info"
-
# warn "You did a bad thing"
-
# end
-
#
-
# assert_match %r%info%, out
-
# assert_match %r%bad%, err
-
#
-
# NOTE: For efficiency, this method uses StringIO and does not
-
# capture IO for subprocesses. Use #capture_subprocess_io for
-
# that.
-
-
1
def capture_io
-
_synchronize do
-
begin
-
captured_stdout, captured_stderr = StringIO.new, StringIO.new
-
-
orig_stdout, orig_stderr = $stdout, $stderr
-
$stdout, $stderr = captured_stdout, captured_stderr
-
-
yield
-
-
return captured_stdout.string, captured_stderr.string
-
ensure
-
$stdout = orig_stdout
-
$stderr = orig_stderr
-
end
-
end
-
end
-
-
##
-
# Captures $stdout and $stderr into strings, using Tempfile to
-
# ensure that subprocess IO is captured as well.
-
#
-
# out, err = capture_subprocess_io do
-
# system "echo Some info"
-
# system "echo You did a bad thing 1>&2"
-
# end
-
#
-
# assert_match %r%info%, out
-
# assert_match %r%bad%, err
-
#
-
# NOTE: This method is approximately 10x slower than #capture_io so
-
# only use it when you need to test the output of a subprocess.
-
-
1
def capture_subprocess_io
-
_synchronize do
-
begin
-
require "tempfile"
-
-
captured_stdout, captured_stderr = Tempfile.new("out"), Tempfile.new("err")
-
-
orig_stdout, orig_stderr = $stdout.dup, $stderr.dup
-
$stdout.reopen captured_stdout
-
$stderr.reopen captured_stderr
-
-
yield
-
-
$stdout.rewind
-
$stderr.rewind
-
-
return captured_stdout.read, captured_stderr.read
-
ensure
-
captured_stdout.unlink
-
captured_stderr.unlink
-
$stdout.reopen orig_stdout
-
$stderr.reopen orig_stderr
-
-
orig_stdout.close
-
orig_stderr.close
-
captured_stdout.close
-
captured_stderr.close
-
end
-
end
-
end
-
-
##
-
# Returns details for exception +e+
-
-
1
def exception_details e, msg
-
[
-
"#{msg}",
-
"Class: <#{e.class}>",
-
"Message: <#{e.message.inspect}>",
-
"---Backtrace---",
-
"#{Minitest.filter_backtrace(e.backtrace).join("\n")}",
-
"---------------",
-
].join "\n"
-
end
-
-
##
-
# Fails after a given date (in the local time zone). This allows
-
# you to put time-bombs in your tests if you need to keep
-
# something around until a later date lest you forget about it.
-
-
1
def fail_after y,m,d,msg
-
flunk msg if Time.now > Time.local(y, m, d)
-
end
-
-
##
-
# Fails with +msg+.
-
-
1
def flunk msg = nil
-
msg ||= "Epic Fail!"
-
assert false, msg
-
end
-
-
##
-
# Returns a proc that will output +msg+ along with the default message.
-
-
1
def message msg = nil, ending = nil, &default
-
1
proc {
-
msg = msg.call.chomp(".") if Proc === msg
-
custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty?
-
"#{custom_message}#{default.call}#{ending || "."}"
-
}
-
end
-
-
##
-
# used for counting assertions
-
-
1
def pass _msg = nil
-
assert true
-
end
-
-
##
-
# Fails if +test+ is truthy.
-
-
1
def refute test, msg = nil
-
msg ||= message { "Expected #{mu_pp(test)} to not be truthy" }
-
assert !test, msg
-
end
-
-
##
-
# Fails if +obj+ is empty.
-
-
1
def refute_empty obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not be empty" }
-
assert_respond_to obj, :empty?
-
refute obj.empty?, msg
-
end
-
-
##
-
# Fails if <tt>exp == act</tt>.
-
#
-
# For floats use refute_in_delta.
-
-
1
def refute_equal exp, act, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(act)} to not be equal to #{mu_pp(exp)}"
-
}
-
refute exp == act, msg
-
end
-
-
##
-
# For comparing Floats. Fails if +exp+ is within +delta+ of +act+.
-
#
-
# refute_in_delta Math::PI, (22.0 / 7.0)
-
-
1
def refute_in_delta exp, act, delta = 0.001, msg = nil
-
n = (exp - act).abs
-
msg = message(msg) {
-
"Expected |#{exp} - #{act}| (#{n}) to not be <= #{delta}"
-
}
-
refute delta >= n, msg
-
end
-
-
##
-
# For comparing Floats. Fails if +exp+ and +act+ have a relative error
-
# less than +epsilon+.
-
-
1
def refute_in_epsilon a, b, epsilon = 0.001, msg = nil
-
refute_in_delta a, b, a * epsilon, msg
-
end
-
-
##
-
# Fails if +collection+ includes +obj+.
-
-
1
def refute_includes collection, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(collection)} to not include #{mu_pp(obj)}"
-
}
-
assert_respond_to collection, :include?
-
refute collection.include?(obj), msg
-
end
-
-
##
-
# Fails if +obj+ is an instance of +cls+.
-
-
1
def refute_instance_of cls, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} to not be an instance of #{cls}"
-
}
-
refute obj.instance_of?(cls), msg
-
end
-
-
##
-
# Fails if +obj+ is a kind of +cls+.
-
-
1
def refute_kind_of cls, obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not be a kind of #{cls}" }
-
refute obj.kind_of?(cls), msg
-
end
-
-
##
-
# Fails if +matcher+ <tt>=~</tt> +obj+.
-
-
1
def refute_match matcher, obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp matcher} to not match #{mu_pp obj}" }
-
assert_respond_to matcher, :"=~"
-
matcher = Regexp.new Regexp.escape matcher if String === matcher
-
refute matcher =~ obj, msg
-
end
-
-
##
-
# Fails if +obj+ is nil.
-
-
1
def refute_nil obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not be nil" }
-
refute obj.nil?, msg
-
end
-
-
##
-
# Fails if +o1+ is not +op+ +o2+. Eg:
-
#
-
# refute_operator 1, :>, 2 #=> pass
-
# refute_operator 1, :<, 2 #=> fail
-
-
1
def refute_operator o1, op, o2 = UNDEFINED, msg = nil
-
return refute_predicate o1, op, msg if UNDEFINED == o2
-
msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op} #{mu_pp(o2)}" }
-
refute o1.__send__(op, o2), msg
-
end
-
-
##
-
# Fails if +path+ exists.
-
-
1
def refute_path_exists path, msg = nil
-
msg = message(msg) { "Expected path '#{path}' to not exist" }
-
refute File.exist?(path), msg
-
end
-
-
##
-
# For testing with predicates.
-
#
-
# refute_predicate str, :empty?
-
#
-
# This is really meant for specs and is front-ended by refute_operator:
-
#
-
# str.wont_be :empty?
-
-
1
def refute_predicate o1, op, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op}" }
-
refute o1.__send__(op), msg
-
end
-
-
##
-
# Fails if +obj+ responds to the message +meth+.
-
-
1
def refute_respond_to obj, meth, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not respond to #{meth}" }
-
-
refute obj.respond_to?(meth), msg
-
end
-
-
##
-
# Fails if +exp+ is the same (by object identity) as +act+.
-
-
1
def refute_same exp, act, msg = nil
-
msg = message(msg) {
-
data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
-
"Expected %s (oid=%d) to not be the same as %s (oid=%d)" % data
-
}
-
refute exp.equal?(act), msg
-
end
-
-
##
-
# Skips the current run. If run in verbose-mode, the skipped run
-
# gets listed at the end of the run but doesn't cause a failure
-
# exit code.
-
-
1
def skip msg = nil, bt = caller
-
msg ||= "Skipped, no message given"
-
@skip = true
-
raise Minitest::Skip, msg, bt
-
end
-
-
##
-
# Skips the current run until a given date (in the local time
-
# zone). This allows you to put some fixes on hold until a later
-
# date, but still holds you accountable and prevents you from
-
# forgetting it.
-
-
1
def skip_until y,m,d,msg
-
skip msg if Time.now < Time.local(y, m, d)
-
where = caller.first.split(/:/, 3).first(2).join ":"
-
warn "Stale skip_until %p at %s" % [msg, where]
-
end
-
-
##
-
# Was this testcase skipped? Meant for #teardown.
-
-
1
def skipped?
-
defined?(@skip) and @skip
-
end
-
end
-
end
-
begin
-
1
require "rubygems"
-
1
gem "minitest"
-
rescue Gem::LoadError
-
# do nothing
-
end
-
-
1
require "minitest"
-
1
require "minitest/spec"
-
1
require "minitest/mock"
-
1
require "minitest/hell" if ENV["MT_HELL"]
-
-
1
Minitest.autorun
-
##
-
# It's where you hide your "assertions".
-
#
-
# Please note, because of the way that expectations are implemented,
-
# all expectations (eg must_equal) are dependent upon a thread local
-
# variable +:current_spec+. If your specs rely on mixing threads into
-
# the specs themselves, you're better off using assertions or the new
-
# _(value) wrapper. For example:
-
#
-
# it "should still work in threads" do
-
# my_threaded_thingy do
-
# (1+1).must_equal 2 # bad
-
# assert_equal 2, 1+1 # good
-
# _(1 + 1).must_equal 2 # good
-
# value(1 + 1).must_equal 2 # good, also #expect
-
# _ { 1 + "1" }.must_raise TypeError # good
-
# end
-
# end
-
-
1
module Minitest::Expectations
-
-
##
-
# See Minitest::Assertions#assert_empty.
-
#
-
# _(collection).must_be_empty
-
#
-
# :method: must_be_empty
-
-
1
infect_an_assertion :assert_empty, :must_be_empty, :unary
-
-
##
-
# See Minitest::Assertions#assert_equal
-
#
-
# _(a).must_equal b
-
#
-
# :method: must_equal
-
-
1
infect_an_assertion :assert_equal, :must_equal
-
-
##
-
# See Minitest::Assertions#assert_in_delta
-
#
-
# _(n).must_be_close_to m [, delta]
-
#
-
# :method: must_be_close_to
-
-
1
infect_an_assertion :assert_in_delta, :must_be_close_to
-
-
1
infect_an_assertion :assert_in_delta, :must_be_within_delta # :nodoc:
-
-
##
-
# See Minitest::Assertions#assert_in_epsilon
-
#
-
# _(n).must_be_within_epsilon m [, epsilon]
-
#
-
# :method: must_be_within_epsilon
-
-
1
infect_an_assertion :assert_in_epsilon, :must_be_within_epsilon
-
-
##
-
# See Minitest::Assertions#assert_includes
-
#
-
# _(collection).must_include obj
-
#
-
# :method: must_include
-
-
1
infect_an_assertion :assert_includes, :must_include, :reverse
-
-
##
-
# See Minitest::Assertions#assert_instance_of
-
#
-
# _(obj).must_be_instance_of klass
-
#
-
# :method: must_be_instance_of
-
-
1
infect_an_assertion :assert_instance_of, :must_be_instance_of
-
-
##
-
# See Minitest::Assertions#assert_kind_of
-
#
-
# _(obj).must_be_kind_of mod
-
#
-
# :method: must_be_kind_of
-
-
1
infect_an_assertion :assert_kind_of, :must_be_kind_of
-
-
##
-
# See Minitest::Assertions#assert_match
-
#
-
# _(a).must_match b
-
#
-
# :method: must_match
-
-
1
infect_an_assertion :assert_match, :must_match
-
-
##
-
# See Minitest::Assertions#assert_nil
-
#
-
# _(obj).must_be_nil
-
#
-
# :method: must_be_nil
-
-
1
infect_an_assertion :assert_nil, :must_be_nil, :unary
-
-
##
-
# See Minitest::Assertions#assert_operator
-
#
-
# _(n).must_be :<=, 42
-
#
-
# This can also do predicates:
-
#
-
# _(str).must_be :empty?
-
#
-
# :method: must_be
-
-
1
infect_an_assertion :assert_operator, :must_be, :reverse
-
-
##
-
# See Minitest::Assertions#assert_output
-
#
-
# _ { ... }.must_output out_or_nil [, err]
-
#
-
# :method: must_output
-
-
1
infect_an_assertion :assert_output, :must_output, :block
-
-
##
-
# See Minitest::Assertions#assert_raises
-
#
-
# _ { ... }.must_raise exception
-
#
-
# :method: must_raise
-
-
1
infect_an_assertion :assert_raises, :must_raise, :block
-
-
##
-
# See Minitest::Assertions#assert_respond_to
-
#
-
# _(obj).must_respond_to msg
-
#
-
# :method: must_respond_to
-
-
1
infect_an_assertion :assert_respond_to, :must_respond_to, :reverse
-
-
##
-
# See Minitest::Assertions#assert_same
-
#
-
# _(a).must_be_same_as b
-
#
-
# :method: must_be_same_as
-
-
1
infect_an_assertion :assert_same, :must_be_same_as
-
-
##
-
# See Minitest::Assertions#assert_silent
-
#
-
# _ { ... }.must_be_silent
-
#
-
# :method: must_be_silent
-
-
1
infect_an_assertion :assert_silent, :must_be_silent, :block
-
-
##
-
# See Minitest::Assertions#assert_throws
-
#
-
# _ { ... }.must_throw sym
-
#
-
# :method: must_throw
-
-
1
infect_an_assertion :assert_throws, :must_throw, :block
-
-
##
-
# See Minitest::Assertions#assert_path_exists
-
#
-
# _(some_path).path_must_exist
-
#
-
# :method: path_must_exist
-
-
1
infect_an_assertion :assert_path_exists, :path_must_exist, :unary
-
-
##
-
# See Minitest::Assertions#refute_path_exists
-
#
-
# _(some_path).path_wont_exist
-
#
-
# :method: path_wont_exist
-
-
1
infect_an_assertion :refute_path_exists, :path_wont_exist, :unary
-
-
##
-
# See Minitest::Assertions#refute_empty
-
#
-
# _(collection).wont_be_empty
-
#
-
# :method: wont_be_empty
-
-
1
infect_an_assertion :refute_empty, :wont_be_empty, :unary
-
-
##
-
# See Minitest::Assertions#refute_equal
-
#
-
# _(a).wont_equal b
-
#
-
# :method: wont_equal
-
-
1
infect_an_assertion :refute_equal, :wont_equal
-
-
##
-
# See Minitest::Assertions#refute_in_delta
-
#
-
# _(n).wont_be_close_to m [, delta]
-
#
-
# :method: wont_be_close_to
-
-
1
infect_an_assertion :refute_in_delta, :wont_be_close_to
-
-
1
infect_an_assertion :refute_in_delta, :wont_be_within_delta # :nodoc:
-
-
##
-
# See Minitest::Assertions#refute_in_epsilon
-
#
-
# _(n).wont_be_within_epsilon m [, epsilon]
-
#
-
# :method: wont_be_within_epsilon
-
-
1
infect_an_assertion :refute_in_epsilon, :wont_be_within_epsilon
-
-
##
-
# See Minitest::Assertions#refute_includes
-
#
-
# _(collection).wont_include obj
-
#
-
# :method: wont_include
-
-
1
infect_an_assertion :refute_includes, :wont_include, :reverse
-
-
##
-
# See Minitest::Assertions#refute_instance_of
-
#
-
# _(obj).wont_be_instance_of klass
-
#
-
# :method: wont_be_instance_of
-
-
1
infect_an_assertion :refute_instance_of, :wont_be_instance_of
-
-
##
-
# See Minitest::Assertions#refute_kind_of
-
#
-
# _(obj).wont_be_kind_of mod
-
#
-
# :method: wont_be_kind_of
-
-
1
infect_an_assertion :refute_kind_of, :wont_be_kind_of
-
-
##
-
# See Minitest::Assertions#refute_match
-
#
-
# _(a).wont_match b
-
#
-
# :method: wont_match
-
-
1
infect_an_assertion :refute_match, :wont_match
-
-
##
-
# See Minitest::Assertions#refute_nil
-
#
-
# _(obj).wont_be_nil
-
#
-
# :method: wont_be_nil
-
-
1
infect_an_assertion :refute_nil, :wont_be_nil, :unary
-
-
##
-
# See Minitest::Assertions#refute_operator
-
#
-
# _(n).wont_be :<=, 42
-
#
-
# This can also do predicates:
-
#
-
# str.wont_be :empty?
-
#
-
# :method: wont_be
-
-
1
infect_an_assertion :refute_operator, :wont_be, :reverse
-
-
##
-
# See Minitest::Assertions#refute_respond_to
-
#
-
# _(obj).wont_respond_to msg
-
#
-
# :method: wont_respond_to
-
-
1
infect_an_assertion :refute_respond_to, :wont_respond_to, :reverse
-
-
##
-
# See Minitest::Assertions#refute_same
-
#
-
# _(a).wont_be_same_as b
-
#
-
# :method: wont_be_same_as
-
-
1
infect_an_assertion :refute_same, :wont_be_same_as
-
end
-
1
class MockExpectationError < StandardError; end # :nodoc:
-
-
1
module Minitest # :nodoc:
-
-
##
-
# A simple and clean mock object framework.
-
#
-
# All mock objects are an instance of Mock
-
-
1
class Mock
-
1
alias :__respond_to? :respond_to?
-
-
overridden_methods = %w[
-
1
===
-
class
-
inspect
-
instance_eval
-
instance_variables
-
object_id
-
public_send
-
respond_to_missing?
-
send
-
to_s
-
]
-
-
1
instance_methods.each do |m|
-
92
undef_method m unless overridden_methods.include?(m.to_s) || m =~ /^__/
-
end
-
-
1
overridden_methods.map(&:to_sym).each do |method_id|
-
10
define_method method_id do |*args, &b|
-
if @expected_calls.key? method_id then
-
method_missing(method_id, *args, &b)
-
else
-
super(*args, &b)
-
end
-
end
-
end
-
-
1
def initialize delegator = nil # :nodoc:
-
@delegator = delegator
-
@expected_calls = Hash.new { |calls, name| calls[name] = [] }
-
@actual_calls = Hash.new { |calls, name| calls[name] = [] }
-
end
-
-
##
-
# Expect that method +name+ is called, optionally with +args+ or a
-
# +blk+, and returns +retval+.
-
#
-
# @mock.expect(:meaning_of_life, 42)
-
# @mock.meaning_of_life # => 42
-
#
-
# @mock.expect(:do_something_with, true, [some_obj, true])
-
# @mock.do_something_with(some_obj, true) # => true
-
#
-
# @mock.expect(:do_something_else, true) do |a1, a2|
-
# a1 == "buggs" && a2 == :bunny
-
# end
-
#
-
# +args+ is compared to the expected args using case equality (ie, the
-
# '===' operator), allowing for less specific expectations.
-
#
-
# @mock.expect(:uses_any_string, true, [String])
-
# @mock.uses_any_string("foo") # => true
-
# @mock.verify # => true
-
#
-
# @mock.expect(:uses_one_string, true, ["foo"])
-
# @mock.uses_one_string("bar") # => raises MockExpectationError
-
#
-
# If a method will be called multiple times, specify a new expect for each one.
-
# They will be used in the order you define them.
-
#
-
# @mock.expect(:ordinal_increment, 'first')
-
# @mock.expect(:ordinal_increment, 'second')
-
#
-
# @mock.ordinal_increment # => 'first'
-
# @mock.ordinal_increment # => 'second'
-
# @mock.ordinal_increment # => raises MockExpectationError "No more expects available for :ordinal_increment"
-
#
-
-
1
def expect name, retval, args = [], &blk
-
name = name.to_sym
-
-
if block_given?
-
raise ArgumentError, "args ignored when block given" unless args.empty?
-
@expected_calls[name] << { :retval => retval, :block => blk }
-
else
-
raise ArgumentError, "args must be an array" unless Array === args
-
@expected_calls[name] << { :retval => retval, :args => args }
-
end
-
self
-
end
-
-
1
def __call name, data # :nodoc:
-
case data
-
when Hash then
-
"#{name}(#{data[:args].inspect[1..-2]}) => #{data[:retval].inspect}"
-
else
-
data.map { |d| __call name, d }.join ", "
-
end
-
end
-
-
##
-
# Verify that all methods were called as expected. Raises
-
# +MockExpectationError+ if the mock object was not called as
-
# expected.
-
-
1
def verify
-
@expected_calls.each do |name, expected|
-
actual = @actual_calls.fetch(name, nil)
-
raise MockExpectationError, "expected #{__call name, expected[0]}" unless actual
-
raise MockExpectationError, "expected #{__call name, expected[actual.size]}, got [#{__call name, actual}]" if
-
actual.size < expected.size
-
end
-
true
-
end
-
-
1
def method_missing sym, *args, &block # :nodoc:
-
unless @expected_calls.key?(sym) then
-
if @delegator && @delegator.respond_to?(sym)
-
return @delegator.public_send(sym, *args, &block)
-
else
-
raise NoMethodError, "unmocked method %p, expected one of %p" %
-
[sym, @expected_calls.keys.sort_by(&:to_s)]
-
end
-
end
-
-
index = @actual_calls[sym].length
-
expected_call = @expected_calls[sym][index]
-
-
unless expected_call then
-
raise MockExpectationError, "No more expects available for %p: %p" %
-
[sym, args]
-
end
-
-
expected_args, retval, val_block =
-
expected_call.values_at(:args, :retval, :block)
-
-
if val_block then
-
# keep "verify" happy
-
@actual_calls[sym] << expected_call
-
-
raise MockExpectationError, "mocked method %p failed block w/ %p" %
-
[sym, args] unless val_block.call(*args, &block)
-
-
return retval
-
end
-
-
if expected_args.size != args.size then
-
raise ArgumentError, "mocked method %p expects %d arguments, got %d" %
-
[sym, expected_args.size, args.size]
-
end
-
-
zipped_args = expected_args.zip(args)
-
fully_matched = zipped_args.all? { |mod, a|
-
mod === a or mod == a
-
}
-
-
unless fully_matched then
-
raise MockExpectationError, "mocked method %p called with unexpected arguments %p" %
-
[sym, args]
-
end
-
-
@actual_calls[sym] << {
-
:retval => retval,
-
:args => zipped_args.map! { |mod, a| mod === a ? mod : a },
-
}
-
-
retval
-
end
-
-
1
def respond_to? sym, include_private = false # :nodoc:
-
return true if @expected_calls.key? sym.to_sym
-
return true if @delegator && @delegator.respond_to?(sym, include_private)
-
__respond_to?(sym, include_private)
-
end
-
end
-
end
-
-
1
module Minitest::Assertions
-
##
-
# Assert that the mock verifies correctly.
-
-
1
def assert_mock mock
-
assert mock.verify
-
end
-
end
-
-
##
-
# Object extensions for Minitest::Mock.
-
-
1
class Object
-
-
##
-
# Add a temporary stubbed method replacing +name+ for the duration
-
# of the +block+. If +val_or_callable+ responds to #call, then it
-
# returns the result of calling it, otherwise returns the value
-
# as-is. If stubbed method yields a block, +block_args+ will be
-
# passed along. Cleans up the stub at the end of the +block+. The
-
# method +name+ must exist before stubbing.
-
#
-
# def test_stale_eh
-
# obj_under_test = Something.new
-
# refute obj_under_test.stale?
-
#
-
# Time.stub :now, Time.at(0) do
-
# assert obj_under_test.stale?
-
# end
-
# end
-
#
-
-
1
def stub name, val_or_callable, *block_args
-
new_name = "__minitest_stub__#{name}"
-
-
metaclass = class << self; self; end
-
-
if respond_to? name and not methods.map(&:to_s).include? name.to_s then
-
metaclass.send :define_method, name do |*args|
-
super(*args)
-
end
-
end
-
-
metaclass.send :alias_method, new_name, name
-
-
metaclass.send :define_method, name do |*args, &blk|
-
if val_or_callable.respond_to? :call then
-
val_or_callable.call(*args, &blk)
-
else
-
blk.call(*block_args) if blk
-
val_or_callable
-
end
-
end
-
-
yield self
-
ensure
-
metaclass.send :undef_method, name
-
metaclass.send :alias_method, name, new_name
-
metaclass.send :undef_method, new_name
-
end
-
end
-
1
module Minitest
-
1
module Parallel #:nodoc:
-
-
##
-
# The engine used to run multiple tests in parallel.
-
-
1
class Executor
-
-
##
-
# The size of the pool of workers.
-
-
1
attr_reader :size
-
-
##
-
# Create a parallel test executor of with +size+ workers.
-
-
1
def initialize size
-
1
@size = size
-
1
@queue = Queue.new
-
1
@pool = nil
-
end
-
-
##
-
# Start the executor
-
-
1
def start
-
1
@pool = size.times.map {
-
2
Thread.new(@queue) do |queue|
-
2
Thread.current.abort_on_exception = true
-
4
while (job = queue.pop)
-
klass, method, reporter = job
-
reporter.synchronize { reporter.prerecord klass, method }
-
result = Minitest.run_one_method klass, method
-
reporter.synchronize { reporter.record result }
-
end
-
end
-
}
-
end
-
-
##
-
# Add a job to the queue
-
-
1
def << work; @queue << work; end
-
-
##
-
# Shuts down the pool of workers by signalling them to quit and
-
# waiting for them all to finish what they're currently working
-
# on.
-
-
1
def shutdown
-
3
size.times { @queue << nil }
-
1
@pool.each(&:join)
-
end
-
end
-
-
1
module Test # :nodoc:
-
1
def _synchronize; Minitest::Test.io_lock.synchronize { yield }; end # :nodoc:
-
-
1
module ClassMethods # :nodoc:
-
1
def run_one_method klass, method_name, reporter
-
Minitest.parallel_executor << [klass, method_name, reporter]
-
end
-
-
1
def test_order
-
:parallel
-
end
-
end
-
end
-
end
-
end
-
1
require "minitest"
-
-
1
module Minitest
-
1
def self.plugin_pride_options opts, _options # :nodoc:
-
1
opts.on "-p", "--pride", "Pride. Show your testing pride!" do
-
PrideIO.pride!
-
end
-
end
-
-
1
def self.plugin_pride_init options # :nodoc:
-
1
if PrideIO.pride? then
-
klass = ENV["TERM"] =~ /^xterm|-256color$/ ? PrideLOL : PrideIO
-
io = klass.new options[:io]
-
-
self.reporter.reporters.grep(Minitest::Reporter).each do |rep|
-
rep.io = io if rep.io.tty?
-
end
-
end
-
end
-
-
##
-
# Show your testing pride!
-
-
1
class PrideIO
-
##
-
# Activate the pride plugin. Called from both -p option and minitest/pride
-
-
1
def self.pride!
-
@pride = true
-
end
-
-
##
-
# Are we showing our testing pride?
-
-
1
def self.pride?
-
1
@pride ||= false
-
end
-
-
# Start an escape sequence
-
1
ESC = "\e["
-
-
# End the escape sequence
-
1
NND = "#{ESC}0m"
-
-
# The IO we're going to pipe through.
-
1
attr_reader :io
-
-
1
def initialize io # :nodoc:
-
@io = io
-
# stolen from /System/Library/Perl/5.10.0/Term/ANSIColor.pm
-
# also reference http://en.wikipedia.org/wiki/ANSI_escape_code
-
@colors ||= (31..36).to_a
-
@size = @colors.size
-
@index = 0
-
end
-
-
##
-
# Wrap print to colorize the output.
-
-
1
def print o
-
case o
-
when "." then
-
io.print pride o
-
when "E", "F" then
-
io.print "#{ESC}41m#{ESC}37m#{o}#{NND}"
-
when "S" then
-
io.print pride o
-
else
-
io.print o
-
end
-
end
-
-
1
def puts *o # :nodoc:
-
o.map! { |s|
-
s.to_s.sub(/Finished/) {
-
@index = 0
-
"Fabulous run".split(//).map { |c|
-
pride(c)
-
}.join
-
}
-
}
-
-
io.puts(*o)
-
end
-
-
##
-
# Color a string.
-
-
1
def pride string
-
string = "*" if string == "."
-
c = @colors[@index % @size]
-
@index += 1
-
"#{ESC}#{c}m#{string}#{NND}"
-
end
-
-
1
def method_missing msg, *args # :nodoc:
-
io.send(msg, *args)
-
end
-
end
-
-
##
-
# If you thought the PrideIO was colorful...
-
#
-
# (Inspired by lolcat, but with clean math)
-
-
1
class PrideLOL < PrideIO
-
1
PI_3 = Math::PI / 3 # :nodoc:
-
-
1
def initialize io # :nodoc:
-
# walk red, green, and blue around a circle separated by equal thirds.
-
#
-
# To visualize, type this into wolfram-alpha:
-
#
-
# plot (3*sin(x)+3), (3*sin(x+2*pi/3)+3), (3*sin(x+4*pi/3)+3)
-
-
# 6 has wide pretty gradients. 3 == lolcat, about half the width
-
@colors = (0...(6 * 7)).map { |n|
-
n *= 1.0 / 6
-
r = (3 * Math.sin(n ) + 3).to_i
-
g = (3 * Math.sin(n + 2 * PI_3) + 3).to_i
-
b = (3 * Math.sin(n + 4 * PI_3) + 3).to_i
-
-
# Then we take rgb and encode them in a single number using base 6.
-
# For some mysterious reason, we add 16... to clear the bottom 4 bits?
-
# Yes... they're ugly.
-
-
36 * r + 6 * g + b + 16
-
}
-
-
super
-
end
-
-
##
-
# Make the string even more colorful. Damnit.
-
-
1
def pride string
-
c = @colors[@index % @size]
-
@index += 1
-
"#{ESC}38;5;#{c}m#{string}#{NND}"
-
end
-
end
-
end
-
1
require "minitest/test"
-
-
1
class Module # :nodoc:
-
1
def infect_an_assertion meth, new_name, dont_flip = false # :nodoc:
-
32
block = dont_flip == :block
-
32
dont_flip = false if block
-
-
# warn "%-22p -> %p %p" % [meth, new_name, dont_flip]
-
32
self.class_eval <<-EOM, __FILE__, __LINE__ + 1
-
def #{new_name} *args
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
warn "DEPRECATED: global use of #{new_name} from #\{where}. Use _(obj).#{new_name} instead. This will fail in Minitest 6."
-
Minitest::Expectation.new(self, Minitest::Spec.current).#{new_name}(*args)
-
end
-
EOM
-
-
32
Minitest::Expectation.class_eval <<-EOM, __FILE__, __LINE__ + 1
-
def #{new_name} *args
-
raise "Calling ##{new_name} outside of test." unless ctx
-
case
-
when #{!!dont_flip} then
-
ctx.#{meth}(target, *args)
-
when #{block} && Proc === target then
-
ctx.#{meth}(*args, &target)
-
else
-
ctx.#{meth}(args.first, target, *args[1..-1])
-
end
-
end
-
EOM
-
end
-
end
-
-
1
Minitest::Expectation = Struct.new :target, :ctx # :nodoc:
-
-
##
-
# Kernel extensions for minitest
-
-
1
module Kernel
-
##
-
# Describe a series of expectations for a given target +desc+.
-
#
-
# Defines a test class subclassing from either Minitest::Spec or
-
# from the surrounding describe's class. The surrounding class may
-
# subclass Minitest::Spec manually in order to easily share code:
-
#
-
# class MySpec < Minitest::Spec
-
# # ... shared code ...
-
# end
-
#
-
# class TestStuff < MySpec
-
# it "does stuff" do
-
# # shared code available here
-
# end
-
# describe "inner stuff" do
-
# it "still does stuff" do
-
# # ...and here
-
# end
-
# end
-
# end
-
#
-
# For more information on getting started with writing specs, see:
-
#
-
# http://www.rubyinside.com/a-minitestspec-tutorial-elegant-spec-style-testing-that-comes-with-ruby-5354.html
-
#
-
# For some suggestions on how to improve your specs, try:
-
#
-
# http://betterspecs.org
-
#
-
# but do note that several items there are debatable or specific to
-
# rspec.
-
#
-
# For more information about expectations, see Minitest::Expectations.
-
-
1
def describe desc, *additional_desc, &block # :doc:
-
stack = Minitest::Spec.describe_stack
-
name = [stack.last, desc, *additional_desc].compact.join("::")
-
sclas = stack.last || if Class === self && kind_of?(Minitest::Spec::DSL) then
-
self
-
else
-
Minitest::Spec.spec_type desc, *additional_desc
-
end
-
-
cls = sclas.create name, desc
-
-
stack.push cls
-
cls.class_eval(&block)
-
stack.pop
-
cls
-
end
-
1
private :describe
-
end
-
-
##
-
# Minitest::Spec -- The faster, better, less-magical spec framework!
-
#
-
# For a list of expectations, see Minitest::Expectations.
-
-
1
class Minitest::Spec < Minitest::Test
-
-
1
def self.current # :nodoc:
-
Thread.current[:current_spec]
-
end
-
-
1
def initialize name # :nodoc:
-
super
-
Thread.current[:current_spec] = self
-
end
-
-
##
-
# Oh look! A Minitest::Spec::DSL module! Eat your heart out DHH.
-
-
1
module DSL
-
##
-
# Contains pairs of matchers and Spec classes to be used to
-
# calculate the superclass of a top-level describe. This allows for
-
# automatically customizable spec types.
-
#
-
# See: register_spec_type and spec_type
-
-
1
TYPES = [[//, Minitest::Spec]]
-
-
##
-
# Register a new type of spec that matches the spec's description.
-
# This method can take either a Regexp and a spec class or a spec
-
# class and a block that takes the description and returns true if
-
# it matches.
-
#
-
# Eg:
-
#
-
# register_spec_type(/Controller$/, Minitest::Spec::Rails)
-
#
-
# or:
-
#
-
# register_spec_type(Minitest::Spec::RailsModel) do |desc|
-
# desc.superclass == ActiveRecord::Base
-
# end
-
-
1
def register_spec_type *args, &block
-
if block then
-
matcher, klass = block, args.first
-
else
-
matcher, klass = *args
-
end
-
TYPES.unshift [matcher, klass]
-
end
-
-
##
-
# Figure out the spec class to use based on a spec's description. Eg:
-
#
-
# spec_type("BlahController") # => Minitest::Spec::Rails
-
-
1
def spec_type desc, *additional
-
TYPES.find { |matcher, _klass|
-
if matcher.respond_to? :call then
-
matcher.call desc, *additional
-
else
-
matcher === desc.to_s
-
end
-
}.last
-
end
-
-
1
def describe_stack # :nodoc:
-
Thread.current[:describe_stack] ||= []
-
end
-
-
1
def children # :nodoc:
-
@children ||= []
-
end
-
-
1
def nuke_test_methods! # :nodoc:
-
self.public_instance_methods.grep(/^test_/).each do |name|
-
self.send :undef_method, name
-
end
-
end
-
-
##
-
# Define a 'before' action. Inherits the way normal methods should.
-
#
-
# NOTE: +type+ is ignored and is only there to make porting easier.
-
#
-
# Equivalent to Minitest::Test#setup.
-
-
1
def before _type = nil, &block
-
define_method :setup do
-
super()
-
self.instance_eval(&block)
-
end
-
end
-
-
##
-
# Define an 'after' action. Inherits the way normal methods should.
-
#
-
# NOTE: +type+ is ignored and is only there to make porting easier.
-
#
-
# Equivalent to Minitest::Test#teardown.
-
-
1
def after _type = nil, &block
-
define_method :teardown do
-
self.instance_eval(&block)
-
super()
-
end
-
end
-
-
##
-
# Define an expectation with name +desc+. Name gets morphed to a
-
# proper test method name. For some freakish reason, people who
-
# write specs don't like class inheritance, so this goes way out of
-
# its way to make sure that expectations aren't inherited.
-
#
-
# This is also aliased to #specify and doesn't require a +desc+ arg.
-
#
-
# Hint: If you _do_ want inheritance, use minitest/test. You can mix
-
# and match between assertions and expectations as much as you want.
-
-
1
def it desc = "anonymous", &block
-
block ||= proc { skip "(no tests defined)" }
-
-
@specs ||= 0
-
@specs += 1
-
-
name = "test_%04d_%s" % [ @specs, desc ]
-
-
undef_klasses = self.children.reject { |c| c.public_method_defined? name }
-
-
define_method name, &block
-
-
undef_klasses.each do |undef_klass|
-
undef_klass.send :undef_method, name
-
end
-
-
name
-
end
-
-
##
-
# Essentially, define an accessor for +name+ with +block+.
-
#
-
# Why use let instead of def? I honestly don't know.
-
-
1
def let name, &block
-
name = name.to_s
-
pre, post = "let '#{name}' cannot ", ". Please use another name."
-
methods = Minitest::Spec.instance_methods.map(&:to_s) - %w[subject]
-
raise ArgumentError, "#{pre}begin with 'test'#{post}" if
-
name =~ /\Atest/
-
raise ArgumentError, "#{pre}override a method in Minitest::Spec#{post}" if
-
methods.include? name
-
-
define_method name do
-
@_memoized ||= {}
-
@_memoized.fetch(name) { |k| @_memoized[k] = instance_eval(&block) }
-
end
-
end
-
-
##
-
# Another lazy man's accessor generator. Made even more lazy by
-
# setting the name for you to +subject+.
-
-
1
def subject &block
-
let :subject, &block
-
end
-
-
1
def create name, desc # :nodoc:
-
cls = Class.new(self) do
-
@name = name
-
@desc = desc
-
-
nuke_test_methods!
-
end
-
-
children << cls
-
-
cls
-
end
-
-
1
def name # :nodoc:
-
defined?(@name) ? @name : super
-
end
-
-
1
def to_s # :nodoc:
-
name # Can't alias due to 1.8.7, not sure why
-
end
-
-
1
attr_reader :desc # :nodoc:
-
1
alias :specify :it
-
-
##
-
# Rdoc... why are you so dumb?
-
-
1
module InstanceMethods
-
##
-
# Takes a value or a block and returns a value monad that has
-
# all of Expectations methods available to it.
-
#
-
# _(1 + 1).must_equal 2
-
#
-
# And for blocks:
-
#
-
# _ { 1 + "1" }.must_raise TypeError
-
#
-
# This method of expectation-based testing is preferable to
-
# straight-expectation methods (on Object) because it stores its
-
# test context, bypassing our hacky use of thread-local variables.
-
#
-
# NOTE: At some point, the methods on Object will be deprecated
-
# and then removed.
-
#
-
# It is also aliased to #value and #expect for your aesthetic
-
# pleasure:
-
#
-
# _(1 + 1).must_equal 2
-
# value(1 + 1).must_equal 2
-
# expect(1 + 1).must_equal 2
-
-
1
def _ value = nil, &block
-
Minitest::Expectation.new block || value, self
-
end
-
-
1
alias value _
-
1
alias expect _
-
-
1
def before_setup # :nodoc:
-
super
-
Thread.current[:current_spec] = self
-
end
-
end
-
-
1
def self.extended obj # :nodoc:
-
1
obj.send :include, InstanceMethods
-
end
-
end
-
-
1
extend DSL
-
-
1
TYPES = DSL::TYPES # :nodoc:
-
end
-
-
1
require "minitest/expectations"
-
-
1
class Object # :nodoc:
-
1
include Minitest::Expectations unless ENV["MT_NO_EXPECTATIONS"]
-
end
-
1
require "minitest" unless defined? Minitest::Runnable
-
-
1
module Minitest
-
##
-
# Subclass Test to create your own tests. Typically you'll want a
-
# Test subclass per implementation class.
-
#
-
# See Minitest::Assertions
-
-
1
class Test < Runnable
-
1
require "minitest/assertions"
-
1
include Minitest::Assertions
-
1
include Minitest::Reportable
-
-
1
def class_name # :nodoc:
-
self.class.name # for Minitest::Reportable
-
end
-
-
1
PASSTHROUGH_EXCEPTIONS = [NoMemoryError, SignalException, SystemExit] # :nodoc:
-
-
# :stopdoc:
-
2
class << self; attr_accessor :io_lock; end
-
1
self.io_lock = Mutex.new
-
# :startdoc:
-
-
##
-
# Call this at the top of your tests when you absolutely
-
# positively need to have ordered tests. In doing so, you're
-
# admitting that you suck and your tests are weak.
-
-
1
def self.i_suck_and_my_tests_are_order_dependent!
-
class << self
-
undef_method :test_order if method_defined? :test_order
-
define_method :test_order do :alpha end
-
end
-
end
-
-
##
-
# Make diffs for this Test use #pretty_inspect so that diff
-
# in assert_equal can have more details. NOTE: this is much slower
-
# than the regular inspect but much more usable for complex
-
# objects.
-
-
1
def self.make_my_diffs_pretty!
-
require "pp"
-
-
define_method :mu_pp, &:pretty_inspect
-
end
-
-
##
-
# Call this at the top of your tests when you want to run your
-
# tests in parallel. In doing so, you're admitting that you rule
-
# and your tests are awesome.
-
-
1
def self.parallelize_me!
-
include Minitest::Parallel::Test
-
extend Minitest::Parallel::Test::ClassMethods
-
end
-
-
##
-
# Returns all instance methods starting with "test_". Based on
-
# #test_order, the methods are either sorted, randomized
-
# (default), or run in parallel.
-
-
1
def self.runnable_methods
-
9
methods = methods_matching(/^test_/)
-
-
9
case self.test_order
-
when :random, :parallel then
-
9
max = methods.size
-
15
methods.sort.sort_by { rand max }
-
when :alpha, :sorted then
-
methods.sort
-
else
-
raise "Unknown test_order: #{self.test_order.inspect}"
-
end
-
end
-
-
##
-
# Defines the order to run tests (:random by default). Override
-
# this or use a convenience method to change it for your tests.
-
-
1
def self.test_order
-
11
:random
-
end
-
-
1
TEARDOWN_METHODS = %w[ before_teardown teardown after_teardown ] # :nodoc:
-
-
##
-
# Runs a single test with setup/teardown hooks.
-
-
1
def run
-
3
with_info_handler do
-
3
time_it do
-
3
capture_exceptions do
-
3
before_setup; setup; after_setup
-
-
3
self.send self.name
-
end
-
-
3
TEARDOWN_METHODS.each do |hook|
-
9
capture_exceptions do
-
9
self.send hook
-
end
-
end
-
end
-
end
-
-
3
Result.from self # per contract
-
end
-
-
##
-
# Provides before/after hooks for setup and teardown. These are
-
# meant for library writers, NOT for regular test authors. See
-
# #before_setup for an example.
-
-
1
module LifecycleHooks
-
-
##
-
# Runs before every test, before setup. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# As a simplistic example:
-
#
-
# module MyMinitestPlugin
-
# def before_setup
-
# super
-
# # ... stuff to do before setup is run
-
# end
-
#
-
# def after_setup
-
# # ... stuff to do after setup is run
-
# super
-
# end
-
#
-
# def before_teardown
-
# super
-
# # ... stuff to do before teardown is run
-
# end
-
#
-
# def after_teardown
-
# # ... stuff to do after teardown is run
-
# super
-
# end
-
# end
-
#
-
# class MiniTest::Test
-
# include MyMinitestPlugin
-
# end
-
-
1
def before_setup; end
-
-
##
-
# Runs before every test. Use this to set up before each test
-
# run.
-
-
1
def setup; end
-
-
##
-
# Runs before every test, after setup. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# See #before_setup for an example.
-
-
1
def after_setup; end
-
-
##
-
# Runs after every test, before teardown. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# See #before_setup for an example.
-
-
1
def before_teardown; end
-
-
##
-
# Runs after every test. Use this to clean up after each test
-
# run.
-
-
1
def teardown; end
-
-
##
-
# Runs after every test, after teardown. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# See #before_setup for an example.
-
-
1
def after_teardown; end
-
end # LifecycleHooks
-
-
1
def capture_exceptions # :nodoc:
-
12
yield
-
rescue *PASSTHROUGH_EXCEPTIONS
-
raise
-
rescue Assertion => e
-
self.failures << e
-
rescue Exception => e
-
self.failures << UnexpectedError.new(e)
-
end
-
-
1
def with_info_handler &block # :nodoc:
-
3
t0 = Minitest.clock_time
-
-
3
handler = lambda do
-
warn "\nCurrent: %s#%s %.2fs" % [self.class, self.name, Minitest.clock_time - t0]
-
end
-
-
3
self.class.on_signal ::Minitest.info_signal, handler, &block
-
end
-
-
1
include LifecycleHooks
-
1
include Guard
-
1
extend Guard
-
end # Test
-
end
-
-
1
require "minitest/unit" unless defined?(MiniTest) # compatibility layer only
-
# :stopdoc:
-
-
1
unless defined?(Minitest) then
-
# all of this crap is just to avoid circular requires and is only
-
# needed if a user requires "minitest/unit" directly instead of
-
# "minitest/autorun", so we also warn
-
-
from = caller.reject { |s| s =~ /rubygems/ }.join("\n ")
-
warn "Warning: you should require 'minitest/autorun' instead."
-
warn %(Warning: or add 'gem "minitest"' before 'require "minitest/autorun"')
-
warn "From:\n #{from}"
-
-
module Minitest; end
-
MiniTest = Minitest # prevents minitest.rb from requiring back to us
-
require "minitest"
-
end
-
-
1
MiniTest = Minitest unless defined?(MiniTest)
-
-
1
module Minitest
-
1
class Unit
-
1
VERSION = Minitest::VERSION
-
1
class TestCase < Minitest::Test
-
1
def self.inherited klass # :nodoc:
-
from = caller.first
-
warn "MiniTest::Unit::TestCase is now Minitest::Test. From #{from}"
-
super
-
end
-
end
-
-
1
def self.autorun # :nodoc:
-
from = caller.first
-
warn "MiniTest::Unit.autorun is now Minitest.autorun. From #{from}"
-
Minitest.autorun
-
end
-
-
1
def self.after_tests &b # :nodoc:
-
from = caller.first
-
warn "MiniTest::Unit.after_tests is now Minitest.after_run. From #{from}"
-
Minitest.after_run(&b)
-
end
-
end
-
end
-
-
# :startdoc:
-
1
require 'multi_json/options'
-
1
require 'multi_json/version'
-
1
require 'multi_json/adapter_error'
-
1
require 'multi_json/parse_error'
-
1
require 'multi_json/options_cache'
-
-
1
module MultiJson
-
1
include Options
-
1
extend self
-
-
1
def default_options=(value)
-
Kernel.warn "MultiJson.default_options setter is deprecated\n" \
-
'Use MultiJson.load_options and MultiJson.dump_options instead'
-
-
self.load_options = self.dump_options = value
-
end
-
-
1
def default_options
-
Kernel.warn "MultiJson.default_options is deprecated\n" \
-
'Use MultiJson.load_options or MultiJson.dump_options instead'
-
-
load_options
-
end
-
-
1
%w(cached_options reset_cached_options!).each do |method_name|
-
2
define_method method_name do |*|
-
Kernel.warn "MultiJson.#{method_name} method is deprecated and no longer used."
-
end
-
end
-
-
1
ALIASES = {'jrjackson' => 'jr_jackson'}
-
-
REQUIREMENT_MAP = [
-
1
[:oj, 'oj'],
-
[:yajl, 'yajl'],
-
[:jr_jackson, 'jrjackson'],
-
[:json_gem, 'json/ext'],
-
[:gson, 'gson'],
-
[:json_pure, 'json/pure'],
-
]
-
-
# The default adapter based on what you currently
-
# have loaded and installed. First checks to see
-
# if any adapters are already loaded, then checks
-
# to see which are installed if none are loaded.
-
1
def default_adapter
-
return :oj if defined?(::Oj)
-
return :yajl if defined?(::Yajl)
-
return :jr_jackson if defined?(::JrJackson)
-
return :json_gem if defined?(::JSON::JSON_LOADED)
-
return :gson if defined?(::Gson)
-
-
REQUIREMENT_MAP.each do |adapter, library|
-
begin
-
require library
-
return adapter
-
rescue ::LoadError
-
next
-
end
-
end
-
-
Kernel.warn '[WARNING] MultiJson is using the default adapter (ok_json). ' \
-
'We recommend loading a different JSON library to improve performance.'
-
-
:ok_json
-
end
-
1
alias_method :default_engine, :default_adapter
-
-
# Get the current adapter class.
-
1
def adapter
-
return @adapter if defined?(@adapter) && @adapter
-
-
use nil # load default adapter
-
-
@adapter
-
end
-
1
alias_method :engine, :adapter
-
-
# Set the JSON parser utilizing a symbol, string, or class.
-
# Supported by default are:
-
#
-
# * <tt>:oj</tt>
-
# * <tt>:json_gem</tt>
-
# * <tt>:json_pure</tt>
-
# * <tt>:ok_json</tt>
-
# * <tt>:yajl</tt>
-
# * <tt>:nsjsonserialization</tt> (MacRuby only)
-
# * <tt>:gson</tt> (JRuby only)
-
# * <tt>:jr_jackson</tt> (JRuby only)
-
1
def use(new_adapter)
-
@adapter = load_adapter(new_adapter)
-
ensure
-
OptionsCache.reset
-
end
-
1
alias_method :adapter=, :use
-
1
alias_method :engine=, :use
-
-
1
def load_adapter(new_adapter)
-
case new_adapter
-
when String, Symbol
-
load_adapter_from_string_name new_adapter.to_s
-
when NilClass, FalseClass
-
load_adapter default_adapter
-
when Class, Module
-
new_adapter
-
else
-
fail ::LoadError, new_adapter
-
end
-
rescue ::LoadError => exception
-
raise AdapterError.build(exception)
-
end
-
-
# Decode a JSON string into Ruby.
-
#
-
# <b>Options</b>
-
#
-
# <tt>:symbolize_keys</tt> :: If true, will use symbols instead of strings for the keys.
-
# <tt>:adapter</tt> :: If set, the selected adapter will be used for this call.
-
1
def load(string, options = {})
-
adapter = current_adapter(options)
-
begin
-
adapter.load(string, options)
-
rescue adapter::ParseError => exception
-
raise ParseError.build(exception, string)
-
end
-
end
-
1
alias_method :decode, :load
-
-
1
def current_adapter(options = {})
-
if (new_adapter = options[:adapter])
-
load_adapter(new_adapter)
-
else
-
adapter
-
end
-
end
-
-
# Encodes a Ruby object as JSON.
-
1
def dump(object, options = {})
-
current_adapter(options).dump(object, options)
-
end
-
1
alias_method :encode, :dump
-
-
# Executes passed block using specified adapter.
-
1
def with_adapter(new_adapter)
-
old_adapter = adapter
-
self.adapter = new_adapter
-
yield
-
ensure
-
self.adapter = old_adapter
-
end
-
1
alias_method :with_engine, :with_adapter
-
-
1
private
-
-
1
def load_adapter_from_string_name(name)
-
name = ALIASES.fetch(name, name)
-
require "multi_json/adapters/#{name.downcase}"
-
klass_name = name.to_s.split('_').map(&:capitalize) * ''
-
MultiJson::Adapters.const_get(klass_name)
-
end
-
end
-
1
module MultiJson
-
1
class AdapterError < ArgumentError
-
1
attr_reader :cause
-
-
1
def self.build(original_exception)
-
message = "Did not recognize your adapter specification (#{original_exception.message})."
-
new(message).tap do |exception|
-
exception.instance_eval do
-
@cause = original_exception
-
set_backtrace original_exception.backtrace
-
end
-
end
-
end
-
end
-
end
-
1
module MultiJson
-
1
module Options
-
1
def load_options=(options)
-
OptionsCache.reset
-
@load_options = options
-
end
-
-
1
def dump_options=(options)
-
OptionsCache.reset
-
@dump_options = options
-
end
-
-
1
def load_options(*args)
-
defined?(@load_options) && get_options(@load_options, *args) || default_load_options
-
end
-
-
1
def dump_options(*args)
-
defined?(@dump_options) && get_options(@dump_options, *args) || default_dump_options
-
end
-
-
1
def default_load_options
-
@default_load_options ||= {}
-
end
-
-
1
def default_dump_options
-
@default_dump_options ||= {}
-
end
-
-
1
private
-
-
1
def get_options(options, *args)
-
if options.respond_to?(:call) && options.arity
-
options.arity == 0 ? options[] : options[*args]
-
elsif options.respond_to?(:to_hash)
-
options.to_hash
-
end
-
end
-
end
-
end
-
1
module MultiJson
-
1
module OptionsCache
-
1
extend self
-
-
1
def reset
-
@dump_cache = {}
-
@load_cache = {}
-
end
-
-
1
def fetch(type, key, &block)
-
cache = instance_variable_get("@#{type}_cache")
-
cache.key?(key) ? cache[key] : write(cache, key, &block)
-
end
-
-
1
private
-
-
# Normally MultiJson is used with a few option sets for both dump/load
-
# methods. When options are generated dynamically though, every call would
-
# cause a cache miss and the cache would grow indefinitely. To prevent
-
# this, we just reset the cache every time the number of keys outgrows
-
# 1000.
-
1
MAX_CACHE_SIZE = 1000
-
-
1
def write(cache, key)
-
cache.clear if cache.length >= MAX_CACHE_SIZE
-
cache[key] = yield
-
end
-
end
-
end
-
1
module MultiJson
-
1
class ParseError < StandardError
-
1
attr_reader :data, :cause
-
-
1
def self.build(original_exception, data)
-
new(original_exception.message).tap do |exception|
-
exception.instance_eval do
-
@cause = original_exception
-
set_backtrace original_exception.backtrace
-
@data = data
-
end
-
end
-
end
-
end
-
-
1
DecodeError = LoadError = ParseError # Legacy support
-
end
-
1
module MultiJson
-
1
class Version
-
1
MAJOR = 1 unless defined? MultiJson::Version::MAJOR
-
1
MINOR = 14 unless defined? MultiJson::Version::MINOR
-
1
PATCH = 1 unless defined? MultiJson::Version::PATCH
-
1
PRE = nil unless defined? MultiJson::Version::PRE
-
-
1
class << self
-
# @return [String]
-
1
def to_s
-
1
[MAJOR, MINOR, PATCH, PRE].compact.join('.')
-
end
-
end
-
end
-
-
1
VERSION = Version.to_s.freeze
-
end
-
# frozen_string_literal: true
-
1
require 'mustermann/pattern'
-
1
require 'mustermann/composite'
-
1
require 'mustermann/concat'
-
1
require 'thread'
-
-
# Namespace and main entry point for the Mustermann library.
-
#
-
# Under normal circumstances the only external API entry point you should be using is {Mustermann.new}.
-
1
module Mustermann
-
# Type to use if no type is given.
-
# @api private
-
1
DEFAULT_TYPE = :sinatra
-
-
# Creates a new pattern based on input.
-
#
-
# * From {Mustermann::Pattern}: returns given pattern.
-
# * From String: creates a pattern from the string, depending on type option (defaults to {Mustermann::Sinatra})
-
# * From Regexp: creates a {Mustermann::Regular} pattern.
-
# * From Symbol: creates a {Mustermann::Sinatra} pattern with a single named capture named after the input.
-
# * From an Array or multiple inputs: creates a new pattern from each element, combines them to a {Mustermann::Composite}.
-
# * From anything else: Will try to call to_pattern on it or raise a TypeError.
-
#
-
# Note that if the input is a {Mustermann::Pattern}, Regexp or Symbol, the type option is ignored and if to_pattern is
-
# called on the object, the type will be handed on but might be ignored by the input object.
-
#
-
# If you want to enforce the pattern type, you should create them via their expected class.
-
#
-
# @example creating patterns
-
# require 'mustermann'
-
#
-
# Mustermann.new("/:name") # => #<Mustermann::Sinatra:"/example">
-
# Mustermann.new("/{name}", type: :template) # => #<Mustermann::Template:"/{name}">
-
# Mustermann.new(/.*/) # => #<Mustermann::Regular:".*">
-
# Mustermann.new(:name, capture: :word) # => #<Mustermann::Sinatra:":name">
-
# Mustermann.new("/", "/*.jpg", type: :shell) # => #<Mustermann::Composite:(shell:"/" | shell:"/*.jpg")>
-
#
-
# @example using custom #to_pattern
-
# require 'mustermann'
-
#
-
# class MyObject
-
# def to_pattern(**options)
-
# Mustermann.new("/:name", **options)
-
# end
-
# end
-
#
-
# Mustermann.new(MyObject.new, type: :rails) # => #<Mustermann::Rails:"/:name">
-
#
-
# @example enforcing type
-
# require 'mustermann/sinatra'
-
#
-
# Mustermann::Sinatra.new("/:name")
-
#
-
# @param [String, Pattern, Regexp, Symbol, #to_pattern, Array<String, Pattern, Regexp, Symbol, #to_pattern>]
-
# input The representation of the pattern
-
# @param [Hash] options The options hash
-
# @return [Mustermann::Pattern] pattern corresponding to string.
-
# @raise (see [])
-
# @raise (see Mustermann::Pattern.new)
-
# @raise [TypeError] if the passed object cannot be converted to a pattern
-
# @see file:README.md#Types_and_Options "Types and Options" in the README
-
1
def self.new(*input, type: DEFAULT_TYPE, operator: :|, **options)
-
20
type ||= DEFAULT_TYPE
-
20
input = input.first if input.size < 2
-
20
case input
-
when Pattern then input
-
6
when Regexp then self[:regexp].new(input, **options)
-
14
when String then self[type].new(input, **options)
-
when Symbol then self[:sinatra].new(input.inspect, **options)
-
when Array then input.map { |i| new(i, type: type, **options) }.inject(operator)
-
else
-
pattern = input.to_pattern(type: type, **options) if input.respond_to? :to_pattern
-
raise TypeError, "#{input.class} can't be coerced into Mustermann::Pattern" if pattern.nil?
-
pattern
-
end
-
end
-
-
1
@mutex ||= Mutex.new
-
1
@types ||= {}
-
-
# Maps a type to its factory.
-
#
-
# @example
-
# Mustermann[:sinatra] # => Mustermann::Sinatra
-
#
-
# @param [Symbol] name a pattern type identifier
-
# @raise [ArgumentError] if the type is not supported
-
# @return [Class, #new] pattern factory
-
1
def self.[](name)
-
20
return name if name.respond_to? :new
-
20
@types.fetch(normalized = normalized_type(name)) do
-
@mutex.synchronize do
-
error = try_require "mustermann/#{normalized}"
-
@types.fetch(normalized) { raise ArgumentError, "unsupported type %p#{" (#{error.message})" if error}" % name }
-
end
-
end
-
end
-
-
# @return [LoadError, nil]
-
# @!visibility private
-
1
def self.try_require(path)
-
require(path)
-
nil
-
rescue LoadError => error
-
raise(error) unless error.path == path
-
error
-
end
-
-
# @!visibility private
-
1
def self.register(name, type)
-
4
@types[normalized_type(name)] = type
-
end
-
-
# @!visibility private
-
1
def self.normalized_type(type)
-
24
type.to_s.gsub('-', '_').downcase
-
end
-
-
# @!visibility private
-
1
def self.extend_object(object)
-
return super unless defined? ::Sinatra::Base and object.is_a? Class and object < ::Sinatra::Base
-
require 'mustermann/extension'
-
object.register Extension
-
end
-
end
-
-
skipped
# :nocov:
-
skipped
begin
-
skipped
require 'mustermann/visualizer' if defined?(Pry) or defined?(IRB)
-
skipped
rescue LoadError => error
-
skipped
raise error unless error.path == 'mustermann/visualizer'
-
skipped
$stderr.puts(error.message) if caller_locations[1].absolute_path =~ %r{/lib/pry/|/irb/|^\((?:irb|pry)\)$}
-
skipped
end
-
skipped
# :nocov:
-
# frozen_string_literal: true
-
1
require 'mustermann/ast/translator'
-
-
1
module Mustermann
-
1
module AST
-
# Make sure #start and #stop is set on every node and within its parents #start and #stop.
-
# @!visibility private
-
1
class Boundaries < Translator
-
# @return [Mustermann::AST::Node] the ast passed as first argument
-
# @!visibility private
-
1
def self.set_boundaries(ast, string: nil, start: 0, stop: string.length)
-
7
new.translate(ast, start, stop)
-
7
ast
-
end
-
-
1
translate(:node) do |start, stop|
-
78
t.set_boundaries(node, start, stop)
-
78
t(payload, node.start, node.stop)
-
end
-
-
1
translate(:with_look_ahead) do |start, stop|
-
t.set_boundaries(node, start, stop)
-
t(head, node.start, node.stop)
-
t(payload, node.start, node.stop)
-
end
-
-
1
translate(Array) do |start, stop|
-
7
each do |subnode|
-
69
t(subnode, start, stop)
-
69
start = subnode.stop
-
end
-
end
-
-
70
translate(Object) { |*| node }
-
-
# Checks that a node is within the given boundaries.
-
# @!visibility private
-
1
def set_boundaries(node, start, stop)
-
78
node.start = start if node.start.nil? or node.start < start
-
78
node.stop = node.start + node.min_size if node.stop.nil? or node.stop < node.start
-
78
node.stop = stop if node.stop > stop
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mustermann/ast/translator'
-
-
1
module Mustermann
-
# @see Mustermann::AST::Pattern
-
1
module AST
-
# Regexp compilation logic.
-
# @!visibility private
-
1
class Compiler < Translator
-
1
raises CompileError
-
-
# Trivial compilations
-
77
translate(Array) { |**o| map { |e| t(e, **o) }.join }
-
8
translate(:node) { |**o| t(payload, **o) }
-
11
translate(:separator) { |**o| Regexp.escape(payload) }
-
3
translate(:optional) { |**o| "(?:%s)?" % t(payload, **o) }
-
59
translate(:char) { |**o| t.encoded(payload, **o) }
-
-
1
translate :union do |**options|
-
"(?:%s)" % payload.map { |e| "(?:%s)" % t(e, **options) }.join(?|)
-
end
-
-
1
translate :expression do |greedy: true, **options|
-
t(payload, allow_reserved: operator.allow_reserved, greedy: greedy && !operator.allow_reserved,
-
parametric: operator.parametric, separator: operator.separator, **options)
-
end
-
-
1
translate :with_look_ahead do |**options|
-
lookahead = each_leaf.inject("") do |ahead, element|
-
ahead + t(element, skip_optional: true, lookahead: ahead, greedy: false, no_captures: true, **options).to_s
-
end
-
lookahead << (at_end ? '$' : '/')
-
t(head, lookahead: lookahead, **options) + t(payload, **options)
-
end
-
-
# Capture compilation is complex. :(
-
# @!visibility private
-
1
class Capture < NodeTranslator
-
1
register :capture
-
-
# @!visibility private
-
1
def translate(**options)
-
2
return pattern(**options) if options[:no_captures]
-
1
"(?<#{name}>#{translate(no_captures: true, **options)})"
-
end
-
-
# @return [String] regexp without the named capture
-
# @!visibility private
-
1
def pattern(capture: nil, **options)
-
1
case capture
-
when Symbol then from_symbol(capture, **options)
-
when Array then from_array(capture, **options)
-
when Hash then from_hash(capture, **options)
-
when String then from_string(capture, **options)
-
1
when nil then from_nil(**options)
-
else capture
-
end
-
end
-
-
1
private
-
2
def qualified(string, greedy: true, **options) "#{string}#{qualifier || "+#{?? unless greedy}"}" end
-
2
def with_lookahead(string, lookahead: nil, **options) lookahead ? "(?:(?!#{lookahead})#{string})" : string end
-
1
def from_hash(hash, **options) pattern(capture: hash[name.to_sym], **options) end
-
1
def from_array(array, **options) Regexp.union(*array.map { |e| pattern(capture: e, **options) }) end
-
1
def from_symbol(symbol, **options) qualified(with_lookahead("[[:#{symbol}:]]", **options), **options) end
-
1
def from_string(string, **options) Regexp.new(string.chars.map { |c| t.encoded(c, **options) }.join) end
-
2
def from_nil(**options) qualified(with_lookahead(default(**options), **options), **options) end
-
2
def default(**options) constraint || "[^/\\?#]" end
-
end
-
-
# @!visibility private
-
1
class Splat < Capture
-
1
register :splat, :named_splat
-
# splats are always non-greedy
-
# @!visibility private
-
1
def pattern(**options)
-
constraint || ".*?"
-
end
-
end
-
-
# @!visibility private
-
1
class Variable < Capture
-
1
register :variable
-
-
# @!visibility private
-
1
def translate(**options)
-
return super(**options) if explode or not options[:parametric]
-
# Remove this line after fixing broken compatibility between 2.1 and 2.2
-
options.delete(:parametric) if options.has_key?(:parametric)
-
parametric super(parametric: false, **options)
-
end
-
-
# @!visibility private
-
1
def pattern(parametric: false, separator: nil, **options)
-
register_param(parametric: parametric, separator: separator, **options)
-
pattern = super(**options)
-
pattern = parametric(pattern) if parametric
-
pattern = "#{pattern}(?:#{Regexp.escape(separator)}#{pattern})*" if explode and separator
-
pattern
-
end
-
-
# @!visibility private
-
1
def parametric(string)
-
"#{Regexp.escape(name)}(?:=#{string})?"
-
end
-
-
# @!visibility private
-
1
def qualified(string, **options)
-
prefix ? "#{string}{1,#{prefix}}" : super(string, **options)
-
end
-
-
# @!visibility private
-
1
def default(allow_reserved: false, **options)
-
allow_reserved ? '[\w\-\.~%\:/\?#\[\]@\!\$\&\'\(\)\*\+,;=]' : '[\w\-\.~%]'
-
end
-
-
# @!visibility private
-
1
def register_param(parametric: false, split_params: nil, separator: nil, **options)
-
return unless explode and split_params
-
split_params[name] = { separator: separator, parametric: parametric }
-
end
-
end
-
-
# @return [String] Regular expression for matching the given character in all representations
-
# @!visibility private
-
1
def encoded(char, uri_decode: true, space_matches_plus: true, **options)
-
58
return Regexp.escape(char) unless uri_decode
-
58
encoded = escape(char, escape: /./)
-
186
list = [escape(char), encoded.downcase, encoded.upcase].uniq.map { |c| Regexp.escape(c) }
-
58
if char == " "
-
list << encoded('+') if space_matches_plus
-
list << " "
-
end
-
58
"(?:%s)" % list.join("|")
-
end
-
-
# Compiles an AST to a regular expression.
-
# @param [Mustermann::AST::Node] ast the tree
-
# @return [Regexp] corresponding regular expression.
-
#
-
# @!visibility private
-
1
def self.compile(ast, **options)
-
7
new.compile(ast, **options)
-
end
-
-
# Compiles an AST to a regular expression.
-
# @param [Mustermann::AST::Node] ast the tree
-
# @return [Regexp] corresponding regular expression.
-
#
-
# @!visibility private
-
1
def compile(ast, except: nil, **options)
-
7
except &&= "(?!#{translate(except, no_captures: true, **options)}\\Z)"
-
7
Regexp.new("#{except}#{translate(ast, **options)}")
-
end
-
end
-
-
1
private_constant :Compiler
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mustermann/ast/translator'
-
1
require 'mustermann/ast/compiler'
-
1
require 'ruby2_keywords'
-
-
1
module Mustermann
-
1
module AST
-
# Looks at an AST, remembers the important bits of information to do an
-
# ultra fast expansion.
-
#
-
# @!visibility private
-
1
class Expander < Translator
-
1
raises ExpandError
-
-
1
translate Array do |*args|
-
inject(t.pattern) do |pattern, element|
-
t.add_to(pattern, t(element, *args))
-
end
-
end
-
-
1
translate :capture do |**options|
-
t.for_capture(node, **options)
-
end
-
-
1
translate :named_splat, :splat do
-
t.pattern + t.for_capture(node)
-
end
-
-
1
translate :expression do
-
t(payload, allow_reserved: operator.allow_reserved)
-
end
-
-
1
translate :root, :group do
-
t(payload)
-
end
-
-
1
translate :char do
-
t.pattern(t.escape(payload, also_escape: /[\/\?#\&\=%]/).gsub(?%, "%%"))
-
end
-
-
1
translate :separator do
-
t.pattern(payload.gsub(?%, "%%"))
-
end
-
-
1
translate :with_look_ahead do
-
t.add_to(t(head), t(payload))
-
end
-
-
1
translate :optional do
-
nested = t(payload)
-
nested += t.pattern unless nested.any? { |n| n.first.empty? }
-
nested
-
end
-
-
1
translate :union do
-
payload.map { |e| t(e) }.inject(:+)
-
end
-
-
# helper method for captures
-
# @!visibility private
-
1
def for_capture(node, **options)
-
name = node.name.to_sym
-
pattern('%s', name, name => /(?!#{pattern_for(node, **options)})./)
-
end
-
-
# maps sorted key list to sprintf patterns and filters
-
# @!visibility private
-
1
def mappings
-
@mappings ||= {}
-
end
-
-
# all the known keys
-
# @!visibility private
-
1
def keys
-
@keys ||= []
-
end
-
-
# add a tree for expansion
-
# @!visibility private
-
1
def add(ast)
-
translate(ast).each do |keys, pattern, filter|
-
self.keys.concat(keys).uniq!
-
mappings[keys.sort] ||= [keys, pattern, filter]
-
end
-
end
-
-
# helper method for getting a capture's pattern.
-
# @!visibility private
-
1
def pattern_for(node, **options)
-
Compiler.new.decorator_for(node).pattern(**options)
-
end
-
-
# @see Mustermann::Pattern#expand
-
# @!visibility private
-
1
def expand(values)
-
adjusted = values.each_with_object({}){ |(key, value), new_hash|
-
new_hash[value.instance_of?(Array) ? [key] * value.length : key] = value }
-
keys, pattern, filters = mappings.fetch(adjusted.keys.flatten.sort) { error_for(values) }
-
filters.each { |key, filter| adjusted[key] &&= escape(adjusted[key], also_escape: filter) }
-
pattern % (adjusted[keys] || adjusted.values_at(*keys))
-
end
-
-
# @see Mustermann::Pattern#expandable?
-
# @!visibility private
-
1
def expandable?(values)
-
values = values.keys if values.respond_to? :keys
-
values = values.sort if values.respond_to? :sort
-
mappings.include? values
-
end
-
-
# @see Mustermann::Expander#with_rest
-
# @!visibility private
-
1
def expandable_keys(keys)
-
mappings.keys.select { |k| (k - keys).empty? }.max_by(&:size) || keys
-
end
-
-
# helper method for raising an error for unexpandable values
-
# @!visibility private
-
1
def error_for(values)
-
expansions = mappings.keys.map(&:inspect).join(" or ")
-
raise error_class, "cannot expand with keys %p, possible expansions: %s" % [values.keys.sort, expansions]
-
end
-
-
# @see Mustermann::AST::Translator#expand
-
# @!visibility private
-
1
ruby2_keywords def escape(string, *args)
-
# URI::Parser is pretty slow, let's not send every string to it, even if it's unnecessary
-
string =~ /\A\w*\Z/ ? string : super
-
end
-
-
# Turns a sprintf pattern into our secret internal data structure.
-
# @!visibility private
-
1
def pattern(string = "", *keys, **filters)
-
[[keys, string, filters]]
-
end
-
-
# Creates the product of two of our secret internal data structures.
-
# @!visibility private
-
1
def add_to(list, result)
-
list << [[], ""] if list.empty?
-
list.inject([]) { |l, (k1, p1, f1)| l + result.map { |k2, p2, f2| [k1+k2, p1+p2, f1.merge(f2)] } }
-
end
-
end
-
end
-
end
-
1
module Mustermann
-
# @see Mustermann::AST::Pattern
-
1
module AST
-
# @!visibility private
-
1
class Node
-
# @!visibility private
-
1
attr_accessor :payload, :start, :stop
-
-
# @!visibility private
-
# @param [Symbol] name of the node
-
# @return [Class] factory for the node
-
1
def self.[](name)
-
354
@names ||= {}
-
354
@names[name] ||= begin
-
8
const_name = constant_name(name)
-
8
Object.const_get(const_name) if Object.const_defined?(const_name)
-
end
-
end
-
-
# Turns a class name into a node identifier.
-
# @!visibility private
-
1
def self.type
-
name[/[^:]+$/].split(/(?<=.)(?=[A-Z])/).map(&:downcase).join(?_).to_sym
-
end
-
-
# @!visibility private
-
# @param [Symbol] name of the node
-
# @return [String] qualified name of factory for the node
-
1
def self.constant_name(name)
-
63
return self.name if name.to_sym == :node
-
57
name = name.to_s.split(?_).map(&:capitalize).join
-
57
"#{self.name}::#{name}"
-
end
-
-
# Helper for creating a new instance and calling #parse on it.
-
# @return [Mustermann::AST::Node]
-
# @!visibility private
-
1
def self.parse(*args, &block)
-
2
new(*args).tap { |n| n.parse(&block) }
-
end
-
-
# @!visibility private
-
1
def initialize(payload = nil, **options)
-
78
options.each { |key, value| public_send("#{key}=", value) }
-
78
self.payload = payload
-
end
-
-
# @!visibility private
-
1
def is_a?(type)
-
285
type = Node[type] if type.is_a? Symbol
-
285
super(type)
-
end
-
-
# Double dispatch helper for reading from the buffer into the payload.
-
# @!visibility private
-
1
def parse
-
8
self.payload ||= []
-
8
while element = yield
-
70
payload << element
-
end
-
end
-
-
# Loop through all nodes that don't have child nodes.
-
# @!visibility private
-
1
def each_leaf(&block)
-
return enum_for(__method__) unless block_given?
-
called = false
-
Array(payload).each do |entry|
-
next unless entry.respond_to? :each_leaf
-
entry.each_leaf(&block)
-
called = true
-
end
-
yield(self) unless called
-
end
-
-
# @return [Integer] length of the substring
-
# @!visibility private
-
1
def length
-
stop - start if start and stop
-
end
-
-
# @return [Integer] minimum size for a node
-
# @!visibility private
-
1
def min_size
-
0
-
end
-
-
# Turns a class name into a node identifier.
-
# @!visibility private
-
1
def type
-
self.class.type
-
end
-
-
# @!visibility private
-
1
class Capture < Node
-
# @see Mustermann::AST::Compiler::Capture#default
-
# @!visibility private
-
1
attr_accessor :constraint
-
-
# @see Mustermann::AST::Compiler::Capture#qualified
-
# @!visibility private
-
1
attr_accessor :qualifier
-
-
# @see Mustermann::AST::Pattern#map_param
-
# @!visibility private
-
1
attr_accessor :convert
-
-
# @see Mustermann::AST::Node#parse
-
# @!visibility private
-
1
def parse
-
1
self.payload ||= ""
-
1
super
-
end
-
-
# @!visibility private
-
1
alias_method :name, :payload
-
end
-
-
# @!visibility private
-
1
class Char < Node
-
# @return [Integer] minimum size for a node
-
# @!visibility private
-
1
def min_size
-
1
-
end
-
end
-
-
# AST node for template expressions.
-
# @!visibility private
-
1
class Expression < Node
-
# @!visibility private
-
1
attr_accessor :operator
-
end
-
-
# @!visibility private
-
1
class Composition < Node
-
# @!visibility private
-
1
def initialize(payload = nil, **options)
-
super(Array(payload), **options)
-
end
-
end
-
-
# @!visibility private
-
1
class Group < Composition
-
end
-
-
# @!visibility private
-
1
class Union < Composition
-
end
-
-
# @!visibility private
-
1
class Optional < Node
-
end
-
-
# @!visibility private
-
1
class Or < Node
-
end
-
-
# @!visibility private
-
1
class Root < Node
-
# @!visibility private
-
1
attr_accessor :pattern
-
-
# Will trigger transform.
-
#
-
# @see Mustermann::AST::Node.parse
-
# @!visibility private
-
1
def self.parse(string, &block)
-
7
root = new
-
7
root.pattern = string
-
7
root.parse(&block)
-
7
root
-
end
-
end
-
-
# @!visibility private
-
1
class Separator < Node
-
# @return [Integer] minimum size for a node
-
# @!visibility private
-
1
def min_size
-
1
-
end
-
end
-
-
# @!visibility private
-
1
class Splat < Capture
-
# @see Mustermann::AST::Node::Capture#name
-
# @!visibility private
-
1
def name
-
"splat"
-
end
-
end
-
-
# @!visibility private
-
1
class NamedSplat < Splat
-
# @see Mustermann::AST::Node::Capture#name
-
# @!visibility private
-
1
alias_method :name, :payload
-
end
-
-
# AST node for template variables.
-
# @!visibility private
-
1
class Variable < Capture
-
# @!visibility private
-
1
attr_accessor :prefix, :explode
-
end
-
-
# @!visibility private
-
1
class WithLookAhead < Node
-
# @!visibility private
-
1
attr_accessor :head, :at_end
-
-
# @!visibility private
-
1
def initialize(payload, at_end, **options)
-
super(**options)
-
self.head, *self.payload = Array(payload)
-
self.at_end = at_end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mustermann/ast/translator'
-
-
1
module Mustermann
-
1
module AST
-
# Scans an AST for param converters.
-
# @!visibility private
-
# @see Mustermann::AST::Pattern#to_templates
-
1
class ParamScanner < Translator
-
# @!visibility private
-
1
def self.scan_params(ast)
-
new.translate(ast)
-
end
-
-
1
translate(:node) { t(payload) }
-
1
translate(Array) { map { |e| t(e) }.inject(:merge) }
-
1
translate(Object) { {} }
-
1
translate(:capture) { convert ? { name => convert } : {} }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mustermann/ast/node'
-
1
require 'forwardable'
-
1
require 'ruby2_keywords'
-
1
require 'strscan'
-
-
1
module Mustermann
-
# @see Mustermann::AST::Pattern
-
1
module AST
-
# Simple, StringScanner based parser.
-
# @!visibility private
-
1
class Parser
-
# @param [String] string to be parsed
-
# @return [Mustermann::AST::Node] parse tree for string
-
# @!visibility private
-
1
def self.parse(string, **options)
-
7
new(**options).parse(string)
-
end
-
-
# Defines another grammar rule for first character.
-
#
-
# @see Mustermann::Rails
-
# @see Mustermann::Sinatra
-
# @see Mustermann::Template
-
# @!visibility private
-
1
def self.on(*chars, &block)
-
7
chars.each do |char|
-
9
define_method("read %p" % char, &block)
-
end
-
end
-
-
# Defines another grammar rule for a suffix.
-
#
-
# @see Mustermann::Sinatra
-
# @!visibility private
-
1
def self.suffix(pattern = /./, after: :node, &block)
-
70
@suffix ||= []
-
70
@suffix << [pattern, after, block] if block
-
70
@suffix
-
end
-
-
# @!visibility private
-
1
attr_reader :buffer, :string, :pattern
-
-
1
extend Forwardable
-
1
def_delegators :buffer, :eos?, :getch, :pos
-
-
# @!visibility private
-
1
def initialize(pattern: nil, **options)
-
7
@pattern = pattern
-
end
-
-
# @param [String] string to be parsed
-
# @return [Mustermann::AST::Node] parse tree for string
-
# @!visibility private
-
1
def parse(string)
-
7
@string = string
-
7
@buffer = ::StringScanner.new(string)
-
83
node(:root, string) { read unless eos? }
-
end
-
-
# @example
-
# node(:char, 'x').compile =~ 'x' # => true
-
#
-
# @param [Symbol] type node type
-
# @return [Mustermann::AST::Node]
-
# @!visibility private
-
1
ruby2_keywords def node(type, *args, &block)
-
78
type = Node[type] unless type.respond_to? :new
-
78
start = pos
-
78
node = block ? type.parse(*args, &block) : type.new(*args)
-
78
min_size(start, pos, node)
-
end
-
-
# Create a node for a character we don't have an explicit rule for.
-
#
-
# @param [String] char the character
-
# @return [Mustermann::AST::Node] the node
-
# @!visibility private
-
1
def default_node(char)
-
68
char == ?/ ? node(:separator, char) : node(:char, char)
-
end
-
-
# Reads the next element from the buffer.
-
# @return [Mustermann::AST::Node] next element
-
# @!visibility private
-
1
def read
-
69
start = pos
-
69
char = getch
-
69
method = "read %p" % char
-
69
element= respond_to?(method) ? send(method, char) : default_node(char)
-
69
min_size(start, pos, element)
-
69
read_suffix(element)
-
end
-
-
# sets start on node to start if it's not set to a lower value.
-
# sets stop on node to stop if it's not set to a higher value.
-
# @return [Mustermann::AST::Node] the node passed as third argument
-
# @!visibility private
-
1
def min_size(start, stop, node)
-
149
stop ||= start
-
149
start ||= stop
-
149
node.start = start unless node.start and node.start < start
-
149
node.stop = stop unless node.stop and node.stop > stop
-
149
node
-
end
-
-
# Checks for a potential suffix on the buffer.
-
# @param [Mustermann::AST::Node] element node without suffix
-
# @return [Mustermann::AST::Node] node with suffix
-
# @!visibility private
-
1
def read_suffix(element)
-
69
self.class.suffix.inject(element) do |ele, (regexp, after, callback)|
-
69
next ele unless ele.is_a?(after) and payload = scan(regexp)
-
2
content = instance_exec(payload, ele, &callback)
-
2
min_size(element.start, pos, content)
-
end
-
end
-
-
# Wrapper around {StringScanner#scan} that turns strings into escaped
-
# regular expressions and returns a MatchData if the regexp has any
-
# named captures.
-
#
-
# @param [Regexp, String] regexp
-
# @see StringScanner#scan
-
# @return [String, MatchData, nil]
-
# @!visibility private
-
1
def scan(regexp)
-
71
regexp = Regexp.new(Regexp.escape(regexp)) unless regexp.is_a? Regexp
-
71
string = buffer.scan(regexp)
-
71
regexp.names.any? ? regexp.match(string) : string
-
end
-
-
# Asserts a regular expression matches what's next on the buffer.
-
# Will return corresponding MatchData if regexp includes named captures.
-
#
-
# @param [Regexp] regexp expected to match
-
# @return [String, MatchData] the match
-
# @raise [Mustermann::ParseError] if expectation wasn't met
-
# @!visibility private
-
1
def expect(regexp, char: nil, **options)
-
scan(regexp) || unexpected(char, **options)
-
end
-
-
# Allows to read a string inside brackets. It does not expect the string
-
# to start with an opening bracket.
-
#
-
# @example
-
# buffer.string = "fo<o>>ba<r>"
-
# read_brackets(?<, ?>) # => "fo<o>"
-
# buffer.rest # => "ba<r>"
-
#
-
# @!visibility private
-
1
def read_brackets(open, close, char: nil, escape: ?\\, quote: false, **options)
-
result = String.new
-
escape = false if escape.nil?
-
while (current = getch)
-
case current
-
when close then return result
-
when open then result << open << read_brackets(open, close) << close
-
when escape then result << escape << getch
-
else result << current
-
end
-
end
-
unexpected(char, **options)
-
end
-
-
-
# Reads an argument string of the format arg1,args2,key:value
-
#
-
# @!visibility private
-
1
def read_args(key_separator, close, separator: ?,, symbol_keys: true, **options)
-
list, map = [], {}
-
while buffer.peek(1) != close
-
scan(separator)
-
entries = read_list(close, separator, separator: key_separator, **options)
-
case entries.size
-
when 1 then list += entries
-
when 2 then map[symbol_keys ? entries.first.to_sym : entries.first] = entries.last
-
else unexpected(key_separator)
-
end
-
buffer.pos -= 1
-
end
-
expect(close)
-
[list, map]
-
end
-
-
# Reads a separated list with the ability to quote, escape and add spaces.
-
#
-
# @!visibility private
-
1
def read_list(*close, separator: ?,, escape: ?\\, quotes: [?", ?'], ignore: " ", **options)
-
result = []
-
while current = getch
-
element = result.empty? ? result : result.last
-
case current
-
when *close then return result
-
when ignore then nil # do nothing
-
when separator then result << String.new
-
when escape then element << getch
-
when *quotes then element << read_escaped(current, escape: escape)
-
else element << current
-
end
-
end
-
unexpected(current, **options)
-
end
-
-
# Read a string until a terminating character, ignoring escaped versions of said character.
-
#
-
# @!visibility private
-
1
def read_escaped(close, escape: ?\\, **options)
-
result = String.new
-
while current = getch
-
case current
-
when close then return result
-
when escape then result << getch
-
else result << current
-
end
-
end
-
unexpected(current, **options)
-
end
-
-
# Helper for raising an exception for an unexpected character.
-
# Will read character from buffer if buffer is passed in.
-
#
-
# @param [String, nil] char the unexpected character
-
# @raise [Mustermann::ParseError, Exception]
-
# @!visibility private
-
1
def unexpected(char = nil, exception: ParseError)
-
char ||= getch
-
char = "space" if char == " "
-
raise exception, "unexpected #{char || "end of string"} while parsing #{string.inspect}"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mustermann/ast/parser'
-
1
require 'mustermann/ast/boundaries'
-
1
require 'mustermann/ast/compiler'
-
1
require 'mustermann/ast/transformer'
-
1
require 'mustermann/ast/validation'
-
1
require 'mustermann/ast/template_generator'
-
1
require 'mustermann/ast/param_scanner'
-
1
require 'mustermann/regexp_based'
-
1
require 'mustermann/expander'
-
1
require 'mustermann/equality_map'
-
-
1
module Mustermann
-
# @see Mustermann::AST::Pattern
-
1
module AST
-
# Superclass for pattern styles that parse an AST from the string pattern.
-
# @abstract
-
1
class Pattern < Mustermann::RegexpBased
-
1
supported_options :capture, :except, :greedy, :space_matches_plus
-
-
1
extend Forwardable, SingleForwardable
-
1
single_delegate on: :parser, suffix: :parser
-
1
instance_delegate %i[parser compiler transformer validation template_generator param_scanner boundaries] => 'self.class'
-
1
instance_delegate parse: :parser, transform: :transformer, validate: :validation,
-
generate_templates: :template_generator, scan_params: :param_scanner, set_boundaries: :boundaries
-
-
# @api private
-
# @return [#parse] parser object for pattern
-
# @!visibility private
-
1
def self.parser
-
7
return Parser if self == AST::Pattern
-
7
const_set :Parser, Class.new(superclass.parser) unless const_defined? :Parser, false
-
7
const_get :Parser
-
end
-
-
# @api private
-
# @return [#compile] compiler object for pattern
-
# @!visibility private
-
1
def self.compiler
-
7
Compiler
-
end
-
-
# @api private
-
# @return [#set_boundaries] translator making sure start and stop is set on all nodes
-
# @!visibility private
-
1
def self.boundaries
-
7
Boundaries
-
end
-
-
# @api private
-
# @return [#transform] transformer object for pattern
-
# @!visibility private
-
1
def self.transformer
-
7
Transformer
-
end
-
-
# @api private
-
# @return [#validate] validation object for pattern
-
# @!visibility private
-
1
def self.validation
-
7
Validation
-
end
-
-
# @api private
-
# @return [#generate_templates] generates URI templates for pattern
-
# @!visibility private
-
1
def self.template_generator
-
TemplateGenerator
-
end
-
-
# @api private
-
# @return [#scan_params] param scanner for pattern
-
# @!visibility private
-
1
def self.param_scanner
-
ParamScanner
-
end
-
-
# @!visibility private
-
1
def compile(**options)
-
7
options[:except] &&= parse options[:except]
-
7
compiler.compile(to_ast, **options)
-
rescue CompileError => error
-
raise error.class, "#{error.message}: #{@string.inspect}", error.backtrace
-
end
-
-
# Internal AST representation of pattern.
-
# @!visibility private
-
1
def to_ast
-
7
@ast_cache ||= EqualityMap.new
-
7
@ast_cache.fetch(@string) do
-
7
ast = parse(@string, pattern: self)
-
7
ast &&= transform(ast)
-
7
ast &&= set_boundaries(ast, string: @string)
-
7
validate(ast)
-
end
-
end
-
-
# All AST-based pattern implementations support expanding.
-
#
-
# @example (see Mustermann::Pattern#expand)
-
# @param (see Mustermann::Pattern#expand)
-
# @return (see Mustermann::Pattern#expand)
-
# @raise (see Mustermann::Pattern#expand)
-
# @see Mustermann::Pattern#expand
-
# @see Mustermann::Expander
-
1
def expand(behavior = nil, values = {})
-
@expander ||= Mustermann::Expander.new(self)
-
@expander.expand(behavior, values)
-
end
-
-
# All AST-based pattern implementations support generating templates.
-
#
-
# @example (see Mustermann::Pattern#to_templates)
-
# @param (see Mustermann::Pattern#to_templates)
-
# @return (see Mustermann::Pattern#to_templates)
-
# @see Mustermann::Pattern#to_templates
-
1
def to_templates
-
@to_templates ||= generate_templates(to_ast)
-
end
-
-
# @!visibility private
-
# @see Mustermann::Pattern#map_param
-
1
def map_param(key, value)
-
return super unless param_converters.include? key
-
param_converters[key][super]
-
end
-
-
# @!visibility private
-
1
def param_converters
-
@param_converters ||= scan_params(to_ast)
-
end
-
-
1
private :compile, :parse, :transform, :validate, :generate_templates, :param_converters, :scan_params, :set_boundaries
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mustermann/ast/translator'
-
-
1
module Mustermann
-
1
module AST
-
# Turns an AST into an Array of URI templates representing the AST.
-
# @!visibility private
-
# @see Mustermann::AST::Pattern#to_templates
-
1
class TemplateGenerator < Translator
-
# @!visibility private
-
1
def self.generate_templates(ast)
-
new.translate(ast).uniq
-
end
-
-
# translate(:expression) is not needed, since template patterns simply call to_s
-
1
translate(:root, :group) { t(payload) || [""] }
-
1
translate(:separator, :char) { t.escape(payload) }
-
1
translate(:capture) { "{#{name}}" }
-
1
translate(:optional) { [t(payload), ""] }
-
1
translate(:named_splat, :splat) { "{+#{name}}" }
-
1
translate(:with_look_ahead) { t([head, payload]) }
-
1
translate(:union) { payload.flat_map { |e| t(e) } }
-
-
1
translate(Array) do
-
map { |e| Array(t(e)) }.inject { |first, second| first.product(second).map(&:join) }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mustermann/ast/translator'
-
-
1
module Mustermann
-
1
module AST
-
# Takes a tree, turns it into an even better tree.
-
# @!visibility private
-
1
class Transformer < Translator
-
-
# Transforms a tree.
-
# @note might mutate handed in tree instead of creating a new one
-
# @param [Mustermann::AST::Node] tree to be transformed
-
# @return [Mustermann::AST::Node] transformed tree
-
# @!visibility private
-
1
def self.transform(tree)
-
7
new.translate(tree)
-
end
-
-
# recursive descent
-
1
translate(:node) do
-
71
node.payload = t(payload)
-
71
node
-
end
-
-
# ignore unknown objects on the tree
-
70
translate(Object) { node }
-
-
# turn a group containing or nodes into a union
-
# @!visibility private
-
1
class GroupTransformer < NodeTranslator
-
1
register :group
-
-
# @!visibility private
-
1
def translate
-
7
payload.flatten! if payload.is_a?(Array)
-
76
return union if payload.any? { |e| e.is_a? :or }
-
7
self.payload = t(payload)
-
7
self
-
end
-
-
# @!visibility private
-
1
def union
-
groups = split_payload.map { |g| group(g) }
-
Node[:union].new(groups, start: node.start, stop: node.stop)
-
end
-
-
# @!visibility private
-
1
def group(elements)
-
return t(elements.first) if elements.size == 1
-
start, stop = elements.first.start, elements.last.stop if elements.any?
-
Node[:group].new(t(elements), start: start, stop: stop)
-
end
-
-
# @!visibility private
-
1
def split_payload
-
groups = [[]]
-
payload.each { |e| e.is_a?(:or) ? groups << [] : groups.last << e }
-
groups.map!
-
end
-
end
-
-
# inject a union node right inside the root node if it contains or nodes
-
# @!visibility private
-
1
class RootTransformer < GroupTransformer
-
1
register :root
-
-
# @!visibility private
-
1
def union
-
self.payload = [super]
-
self
-
end
-
end
-
-
# URI expression transformations depending on operator
-
# @!visibility private
-
1
class ExpressionTransform < NodeTranslator
-
1
register :expression
-
-
# @!visibility private
-
1
Operator ||= Struct.new(:separator, :allow_reserved, :prefix, :parametric)
-
-
# Operators available for expressions.
-
# @!visibility private
-
1
OPERATORS ||= {
-
nil => Operator.new(?,, false, false, false), ?+ => Operator.new(?,, true, false, false),
-
?# => Operator.new(?,, true, ?#, false), ?. => Operator.new(?., false, ?., false),
-
?/ => Operator.new(?/, false, ?/, false), ?; => Operator.new(?;, false, ?;, true),
-
?? => Operator.new(?&, false, ??, true), ?& => Operator.new(?&, false, ?&, true)
-
}
-
-
# Sets operator and inserts separators in between variables.
-
# @!visibility private
-
1
def translate
-
self.operator = OPERATORS.fetch(operator) { raise CompileError, "#{operator} operator not supported" }
-
separator = Node[:separator].new(operator.separator)
-
prefix = Node[:separator].new(operator.prefix)
-
self.payload = Array(payload.inject { |list, element| Array(list) << t(separator.dup) << t(element) })
-
payload.unshift(prefix) if operator.prefix
-
self
-
end
-
end
-
-
# Inserts with_look_ahead nodes wherever appropriate
-
# @!visibility private
-
1
class ArrayTransform < NodeTranslator
-
1
register Array
-
-
# the new array
-
# @!visibility private
-
1
def payload
-
75
@payload ||= []
-
end
-
-
# buffer for potential look ahead
-
# @!visibility private
-
1
def lookahead_buffer
-
79
@lookahead_buffer ||= []
-
end
-
-
# transform the array
-
# @!visibility private
-
1
def translate
-
76
each { |e| track t(e) }
-
7
payload.concat create_lookahead(lookahead_buffer, true)
-
end
-
-
# handle a single element from the array
-
# @!visibility private
-
1
def track(element)
-
69
return list_for(element) << element if lookahead_buffer.empty?
-
1
return lookahead_buffer << element if lookahead? element
-
-
1
lookahead = lookahead_buffer.dup
-
1
lookahead = create_lookahead(lookahead, false) if element.is_a? Node[:separator]
-
1
lookahead_buffer.clear
-
-
1
payload.concat(lookahead) << element
-
end
-
-
# turn look ahead buffer into look ahead node
-
# @!visibility private
-
1
def create_lookahead(elements, *args)
-
7
return elements unless elements.size > 1
-
[Node[:with_look_ahead].new(elements, *args, start: elements.first.start, stop: elements.last.stop)]
-
end
-
-
# can the given element be used in a look-ahead?
-
# @!visibility private
-
1
def lookahead?(element, in_lookahead = false)
-
1
case element
-
1
when Node[:char] then in_lookahead
-
when Node[:group] then lookahead_payload?(element.payload, in_lookahead)
-
when Node[:optional] then lookahead?(element.payload, true) or expect_lookahead?(element.payload)
-
end
-
end
-
-
# does the list of elements look look-ahead-ish to you?
-
# @!visibility private
-
1
def lookahead_payload?(payload, in_lookahead)
-
return unless payload[0..-2].all? { |e| lookahead?(e, in_lookahead) }
-
expect_lookahead?(payload.last) or lookahead?(payload.last, in_lookahead)
-
end
-
-
# can the current element deal with a look-ahead?
-
# @!visibility private
-
1
def expect_lookahead?(element)
-
68
return element.class == Node[:capture] unless element.is_a? Node[:group]
-
element.payload.all? { |e| expect_lookahead?(e) }
-
end
-
-
# helper method for deciding where to put an element for now
-
# @!visibility private
-
1
def list_for(element)
-
68
expect_lookahead?(element) ? lookahead_buffer : payload
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mustermann/ast/node'
-
1
require 'mustermann/error'
-
1
require 'ruby2_keywords'
-
1
require 'delegate'
-
-
1
module Mustermann
-
1
module AST
-
# Implements translator pattern
-
#
-
# @abstract
-
# @!visibility private
-
1
class Translator
-
# Encapsulates a single node translation
-
# @!visibility private
-
1
class NodeTranslator < DelegateClass(Node)
-
# @param [Array<Symbol, Class>] types list of types to register for.
-
# @!visibility private
-
1
def self.register(*types)
-
62
types.each do |type|
-
72
type = Node.constant_name(type) if type.is_a? Symbol
-
72
translator.dispatch_table[type.to_s] = self
-
end
-
end
-
-
# @param node [Mustermann::AST::Node, Object]
-
# @param translator [Mustermann::AST::Translator]
-
#
-
# @!visibility private
-
1
def initialize(node, translator)
-
546
@translator = translator
-
546
super(node)
-
end
-
-
# @!visibility private
-
1
attr_reader :translator
-
-
# shorthand for translating a nested object
-
# @!visibility private
-
1
ruby2_keywords def t(*args, &block)
-
655
return translator unless args.any?
-
518
translator.translate(*args, &block)
-
end
-
-
# @!visibility private
-
1
alias_method :node, :__getobj__
-
end
-
-
# maps types to translations
-
# @!visibility private
-
1
def self.dispatch_table
-
1469
@dispatch_table ||= {}
-
end
-
-
# some magic sauce so {NodeTranslator}s know whom to talk to for {#register}
-
# @!visibility private
-
1
def self.inherited(subclass)
-
9
node_translator = Class.new(NodeTranslator)
-
81
node_translator.define_singleton_method(:translator) { subclass }
-
9
subclass.const_set(:NodeTranslator, node_translator)
-
9
super
-
end
-
-
# DSL-ish method for specifying the exception class to use.
-
# @!visibility private
-
1
def self.raises(error)
-
3
define_method(:error_class) { error }
-
end
-
-
# DSL method for defining single method translations.
-
# @!visibility private
-
1
def self.translate(*types, &block)
-
55
Class.new(const_get(:NodeTranslator)) do
-
55
register(*types)
-
55
define_method(:translate, &block)
-
end
-
end
-
-
# Enables quick creation of a translator object.
-
#
-
# @example
-
# require 'mustermann'
-
# require 'mustermann/ast/translator'
-
#
-
# translator = Mustermann::AST::Translator.create do
-
# translate(:node) { [type, *t(payload)].flatten.compact }
-
# translate(Array) { map { |e| t(e) } }
-
# translate(Object) { }
-
# end
-
#
-
# ast = Mustermann.new('/:name').to_ast
-
# translator.translate(ast) # => [:root, :separator, :capture]
-
#
-
# @!visibility private
-
1
def self.create(&block)
-
1
Class.new(self, &block).new
-
end
-
-
1
raises Mustermann::Error
-
-
# @param [Mustermann::AST::Node, Object] node to translate
-
# @return decorator encapsulating translation
-
#
-
# @!visibility private
-
1
def decorator_for(node)
-
4602
factory = node.class.ancestors.inject(nil) { |d,a| d || self.class.dispatch_table[a.name] }
-
546
raise error_class, "#{self.class}: Cannot translate #{node.class}" unless factory
-
546
factory.new(node, self)
-
end
-
-
# Start the translation dance for a (sub)tree.
-
# @!visibility private
-
1
ruby2_keywords def translate(node, *args, &block)
-
546
result = decorator_for(node).translate(*args, &block)
-
546
result = result.node while result.is_a? NodeTranslator
-
546
result
-
end
-
-
# @return [String] escaped character
-
# @!visibility private
-
1
def escape(char, parser: URI::DEFAULT_PARSER, escape: parser.regexp[:UNSAFE], also_escape: nil)
-
116
escape = Regexp.union(also_escape, escape) if also_escape
-
116
char =~ escape ? parser.escape(char, Regexp.union(*escape)) : char
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mustermann/ast/translator'
-
-
1
module Mustermann
-
1
module AST
-
# Checks the AST for certain validations, like correct capture names.
-
#
-
# Internally a poor man's visitor (abusing translator to not have to implement a visitor).
-
# @!visibility private
-
1
class Validation < Translator
-
# Runs validations.
-
#
-
# @param [Mustermann::AST::Node] ast to be validated
-
# @return [Mustermann::AST::Node] the validated ast
-
# @raise [Mustermann::AST::CompileError] if validation fails
-
# @!visibility private
-
1
def self.validate(ast)
-
7
new.translate(ast)
-
7
ast
-
end
-
-
1
translate(Object, :splat) {}
-
78
translate(:node) { t(payload) }
-
77
translate(Array) { each { |p| t(p)} }
-
2
translate(:capture) { t.check_name(name, forbidden: ['captures', 'splat'])}
-
1
translate(:variable, :named_splat) { t.check_name(name, forbidden: 'captures')}
-
-
# @raise [Mustermann::CompileError] if name is not acceptable
-
# @!visibility private
-
1
def check_name(name, forbidden: [])
-
1
raise CompileError, "capture name can't be empty" if name.nil? or name.empty?
-
1
raise CompileError, "capture name must start with underscore or lower case letter" unless name =~ /^[a-z_]/
-
1
raise CompileError, "capture name can't be #{name}" if Array(forbidden).include? name
-
1
raise CompileError, "can't use the same capture name twice" if names.include? name
-
1
names << name
-
end
-
-
# @return [Array<String>] list of capture names in tree
-
# @!visibility private
-
1
def names
-
2
@names ||= []
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'delegate'
-
-
1
module Mustermann
-
# Class for defining and running simple Hash transformations.
-
#
-
# @example
-
# caster = Mustermann::Caster.new
-
# caster.register(:foo) { |value| { bar: value.upcase } }
-
# caster.cast(foo: "hello", baz: "world") # => { bar: "HELLO", baz: "world" }
-
#
-
# @see Mustermann::Expander#cast
-
#
-
# @!visibility private
-
1
class Caster < DelegateClass(Array)
-
# @param (see #register)
-
# @!visibility private
-
1
def initialize(*types, &block)
-
super([])
-
register(*types, &block)
-
end
-
-
# @param [Array<Symbol, Regexp, #cast, #===>] types identifier for cast type (some need block)
-
# @!visibility private
-
1
def register(*types, &block)
-
return if types.empty? and block.nil?
-
types << Any.new(&block) if types.empty?
-
types.each { |type| self << caster_for(type, &block) }
-
end
-
-
# @param [Symbol, Regexp, #cast, #===] type identifier for cast type (some need block)
-
# @return [#cast] specific cast operation
-
# @!visibility private
-
1
def caster_for(type, &block)
-
case type
-
when Symbol, Regexp then Key.new(type, &block)
-
else type.respond_to?(:cast) ? type : Value.new(type, &block)
-
end
-
end
-
-
# Transforms a Hash.
-
# @param [Hash] hash pre-transform Hash
-
# @return [Hash] post-transform Hash
-
# @!visibility private
-
1
def cast(hash)
-
return hash if empty?
-
merge = {}
-
hash.delete_if do |key, value|
-
next unless casted = lazy.map { |e| e.cast(key, value) }.detect { |e| e }
-
casted = { key => casted } unless casted.respond_to? :to_hash
-
merge.update(casted.to_hash)
-
end
-
hash.update(merge)
-
end
-
-
# Class for block based casts that are triggered for every key/value pair.
-
# @!visibility private
-
1
class Any
-
# @!visibility private
-
1
def initialize(&block)
-
@block = block
-
end
-
-
# @see Mustermann::Caster#cast
-
# @!visibility private
-
1
def cast(key, value)
-
case @block.arity
-
when 0 then @block.call
-
when 1 then @block.call(value)
-
else @block.call(key, value)
-
end
-
end
-
end
-
-
# Class for block based casts that are triggered for key/value pairs with a matching value.
-
# @!visibility private
-
1
class Value < Any
-
# @param [#===] type used for matching values
-
# @!visibility private
-
1
def initialize(type, &block)
-
@type = type
-
super(&block)
-
end
-
-
# @see Mustermann::Caster#cast
-
# @!visibility private
-
1
def cast(key, value)
-
super if @type === value
-
end
-
end
-
-
# Class for block based casts that are triggered for key/value pairs with a matching key.
-
# @!visibility private
-
1
class Key < Any
-
# @param [#===] type used for matching keys
-
# @!visibility private
-
1
def initialize(type, &block)
-
@type = type
-
super(&block)
-
end
-
-
# @see Mustermann::Caster#cast
-
# @!visibility private
-
1
def cast(key, value)
-
super if @type === key
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mustermann
-
# Class for pattern objects composed of multiple patterns using binary logic.
-
# @see Mustermann::Pattern#&
-
# @see Mustermann::Pattern#|
-
# @see Mustermann::Pattern#^
-
1
class Composite < Pattern
-
1
attr_reader :patterns, :operator
-
1
supported_options :operator, :type
-
-
# @see Mustermann::Pattern.supported?
-
1
def self.supported?(option, type: nil, **options)
-
return true if super
-
Mustermann[type || Mustermann::DEFAULT_TYPE].supported?(option, **options)
-
end
-
-
# @return [Mustermann::Pattern] a new composite pattern
-
1
def self.new(*patterns, **options)
-
patterns = patterns.flatten
-
case patterns.size
-
when 0 then raise ArgumentError, 'cannot create empty composite pattern'
-
when 1 then patterns.first
-
else super(patterns, **options)
-
end
-
end
-
-
1
def initialize(patterns, operator: :|, **options)
-
@operator = operator.to_sym
-
@patterns = patterns.flat_map { |p| patterns_from(p, **options) }
-
end
-
-
# @see Mustermann::Pattern#==
-
1
def ==(pattern)
-
patterns == patterns_from(pattern)
-
end
-
-
# @see Mustermann::Pattern#eql?
-
1
def eql?(pattern)
-
patterns.eql? patterns_from(pattern)
-
end
-
-
# @see Mustermann::Pattern#hash
-
1
def hash
-
patterns.hash | operator.hash
-
end
-
-
# @see Mustermann::Pattern#===
-
1
def ===(string)
-
patterns.map { |p| p === string }.inject(operator)
-
end
-
-
# @see Mustermann::Pattern#params
-
1
def params(string)
-
with_matching(string, :params)
-
end
-
-
# @see Mustermann::Pattern#match
-
1
def match(string)
-
with_matching(string, :match)
-
end
-
-
# @!visibility private
-
1
def respond_to_special?(method)
-
return false unless operator == :|
-
patterns.all? { |p| p.respond_to?(method) }
-
end
-
-
# (see Mustermann::Pattern#expand)
-
1
def expand(behavior = nil, values = {})
-
raise NotImplementedError, 'expanding not supported' unless respond_to? :expand
-
@expander ||= Mustermann::Expander.new(*patterns)
-
@expander.expand(behavior, values)
-
end
-
-
# (see Mustermann::Pattern#to_templates)
-
1
def to_templates
-
raise NotImplementedError, 'template generation not supported' unless respond_to? :to_templates
-
patterns.flat_map(&:to_templates).uniq
-
end
-
-
# @return [String] the string representation of the pattern
-
1
def to_s
-
simple_inspect
-
end
-
-
# @!visibility private
-
1
def inspect
-
"#<%p:%s>" % [self.class, simple_inspect]
-
end
-
-
# @!visibility private
-
1
def simple_inspect
-
pattern_strings = patterns.map { |p| p.simple_inspect }
-
"(#{pattern_strings.join(" #{operator} ")})"
-
end
-
-
# @!visibility private
-
1
def with_matching(string, method)
-
return unless self === string
-
pattern = patterns.detect { |p| p === string }
-
pattern.public_send(method, string) if pattern
-
end
-
-
# @!visibility private
-
1
def patterns_from(pattern, **options)
-
return pattern.patterns if pattern.is_a? Composite and pattern.operator == self.operator
-
[options.empty? && pattern.is_a?(Pattern) ? pattern : Mustermann.new(pattern, **options)]
-
end
-
-
1
private :with_matching, :patterns_from
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mustermann
-
# Class for pattern objects that are a concatenation of other patterns.
-
# @see Mustermann::Pattern#+
-
1
class Concat < Composite
-
# Mixin for patterns to support native concatenation.
-
# @!visibility private
-
1
module Native
-
# @see Mustermann::Pattern#+
-
# @!visibility private
-
1
def +(other)
-
other &&= Mustermann.new(other, type: :identity, **options)
-
if (patterns = look_ahead(other)) && !patterns.empty?
-
concat = (self + patterns.inject(:+))
-
concat + other.patterns.slice(patterns.length..-1).inject(:+)
-
else
-
return super unless native = native_concat(other)
-
self.class.new(native, **options)
-
end
-
end
-
-
# @!visibility private
-
1
def look_ahead(other)
-
return unless other.is_a?(Concat)
-
other.patterns.take_while(&method(:native_concat?))
-
end
-
-
# @!visibility private
-
1
def native_concat(other)
-
"#{self}#{other}" if native_concat?(other)
-
end
-
-
# @!visibility private
-
1
def native_concat?(other)
-
other.class == self.class and other.options == options
-
end
-
-
1
private :native_concat, :native_concat?
-
end
-
-
# Should not be used directly.
-
# @!visibility private
-
1
def initialize(*, **)
-
super
-
AST::Validation.validate(combined_ast) if respond_to? :expand
-
end
-
-
# @see Mustermann::Composite#operator
-
# @return [Symbol] always :+
-
1
def operator
-
:+
-
end
-
-
# @see Mustermann::Pattern#===
-
1
def ===(string)
-
peek_size(string) == string.size
-
end
-
-
# @see Mustermann::Pattern#match
-
1
def match(string)
-
peeked = peek_match(string)
-
peeked if peeked.to_s == string
-
end
-
-
# @see Mustermann::Pattern#params
-
1
def params(string)
-
params, size = peek_params(string)
-
params if size == string.size
-
end
-
-
# @see Mustermann::Pattern#peek_size
-
1
def peek_size(string)
-
pump(string) { |p,s| p.peek_size(s) }
-
end
-
-
# @see Mustermann::Pattern#peek_match
-
1
def peek_match(string)
-
pump(string, initial: SimpleMatch.new) do |pattern, substring|
-
return unless match = pattern.peek_match(substring)
-
[match, match.to_s.size]
-
end
-
end
-
-
# @see Mustermann::Pattern#peek_params
-
1
def peek_params(string)
-
pump(string, inject_with: :merge, with_size: true) { |p, s| p.peek_params(s) }
-
end
-
-
# (see Mustermann::Pattern#expand)
-
1
def expand(behavior = nil, values = {})
-
raise NotImplementedError, 'expanding not supported' unless respond_to? :expand
-
@expander ||= Mustermann::Expander.new(self) { combined_ast }
-
@expander.expand(behavior, values)
-
end
-
-
# (see Mustermann::Pattern#to_templates)
-
1
def to_templates
-
raise NotImplementedError, 'template generation not supported' unless respond_to? :to_templates
-
@to_templates ||= patterns.inject(['']) { |list, pattern| list.product(pattern.to_templates).map(&:join) }.uniq
-
end
-
-
# @!visibility private
-
1
def respond_to_special?(method)
-
method = :to_ast if method.to_sym == :expand
-
patterns.all? { |p| p.respond_to?(method) }
-
end
-
-
# used to generate results for various methods by scanning through an input string
-
# @!visibility private
-
1
def pump(string, inject_with: :+, initial: nil, with_size: false)
-
substring = string
-
results = Array(initial)
-
-
patterns.each do |pattern|
-
result, size = yield(pattern, substring)
-
return unless result
-
results << result
-
size ||= result
-
substring = substring[size..-1]
-
end
-
-
results = results.inject(inject_with)
-
with_size ? [results, string.size - substring.size] : results
-
end
-
-
# generates one big AST from all patterns
-
# will not check if patterns support AST generation
-
# @!visibility private
-
1
def combined_ast
-
payload = patterns.map { |p| AST::Node[:group].new(p.to_ast.payload) }
-
AST::Node[:root].new(payload)
-
end
-
-
1
private :combined_ast, :pump
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mustermann
-
# A simple wrapper around ObjectSpace::WeakMap that allows matching keys by equality rather than identity.
-
# Used for caching. Note that `fetch` is not guaranteed to return the object, even if it has not been
-
# garbage collected yet, especially when used concurrently. Therefore, the block passed to `fetch` has to
-
# be idempotent.
-
#
-
# @example
-
# class ExpensiveComputation
-
# @map = Mustermann::EqualityMap.new
-
#
-
# def self.new(*args)
-
# @map.fetch(args) { super }
-
# end
-
# end
-
#
-
# @see #fetch
-
1
class EqualityMap
-
1
attr_reader :map
-
-
1
def self.new
-
9
defined?(ObjectSpace::WeakMap) ? super : {}
-
end
-
-
1
def initialize
-
9
@keys = {}
-
9
@map = ObjectSpace::WeakMap.new
-
end
-
-
# @param [#hash] key for caching
-
# @yield block that will be called to populate entry if missing (has to be idempotent)
-
# @return value stored in map or result of block
-
1
def fetch(key)
-
27
identity = @keys[key.hash]
-
27
if identity == key
-
12
key = identity
-
15
elsif key.frozen?
-
key = key.dup
-
end
-
-
# it is ok that this is not thread-safe, worst case it has double cost in
-
# generating, object equality is not guaranteed anyways
-
27
@map[key] ||= track(key, yield)
-
end
-
-
# @param [#hash] key for identifying the object
-
# @param [Object] object to be stored
-
# @return [Object] same as the second parameter
-
1
def track(key, object)
-
15
object = object.dup if object.frozen?
-
15
ObjectSpace.define_finalizer(object, finalizer(key.hash))
-
15
@keys[key.hash] = key
-
15
object
-
end
-
-
# Finalizer proc needs to be generated in different scope so it doesn't keep a reference to the object.
-
#
-
# @param [Integer] hash for key
-
# @return [Proc] finalizer callback
-
1
def finalizer(hash)
-
21
proc { @keys.delete(hash) }
-
end
-
-
1
private :track, :finalizer
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mustermann
-
1
unless defined?(Mustermann::Error)
-
1
Error = Class.new(StandardError) # Raised if anything goes wrong while generating a {Pattern}.
-
1
CompileError = Class.new(Error) # Raised if anything goes wrong while compiling a {Pattern}.
-
1
ParseError = Class.new(Error) # Raised if anything goes wrong while parsing a {Pattern}.
-
1
ExpandError = Class.new(Error) # Raised if anything goes wrong while expanding a {Pattern}.
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mustermann/ast/expander'
-
1
require 'mustermann/caster'
-
1
require 'mustermann'
-
-
1
module Mustermann
-
# Allows fine-grained control over pattern expansion.
-
#
-
# @example
-
# expander = Mustermann::Expander.new(additional_values: :append)
-
# expander << "/users/:user_id"
-
# expander << "/pages/:page_id"
-
#
-
# expander.expand(page_id: 58, format: :html5) # => "/pages/58?format=html5"
-
1
class Expander
-
1
attr_reader :patterns, :additional_values, :caster
-
-
# @param [Array<#to_str, Mustermann::Pattern>] patterns list of patterns to expand, see {#add}.
-
# @param [Symbol] additional_values behavior when encountering additional values, see {#expand}.
-
# @param [Hash] options used when creating/expanding patterns, see {Mustermann.new}.
-
1
def initialize(*patterns, additional_values: :raise, **options, &block)
-
unless additional_values == :raise or additional_values == :ignore or additional_values == :append
-
raise ArgumentError, "Illegal value %p for additional_values" % additional_values
-
end
-
-
@patterns = []
-
@api_expander = AST::Expander.new
-
@additional_values = additional_values
-
@options = options
-
@caster = Caster.new
-
add(*patterns, &block)
-
end
-
-
# Add patterns to expand.
-
#
-
# @example
-
# expander = Mustermann::Expander.new
-
# expander.add("/:a.jpg", "/:b.png")
-
# expander.expand(a: "pony") # => "/pony.jpg"
-
#
-
# @param [Array<#to_str, Mustermann::Pattern>] patterns list of to add for expansion, Strings will be compiled to patterns.
-
# @return [Mustermann::Expander] the expander
-
1
def add(*patterns)
-
patterns.each do |pattern|
-
pattern = Mustermann.new(pattern, **@options)
-
if block_given?
-
@api_expander.add(yield(pattern))
-
else
-
raise NotImplementedError, "expanding not supported for #{pattern.class}" unless pattern.respond_to? :to_ast
-
@api_expander.add(pattern.to_ast)
-
end
-
@patterns << pattern
-
end
-
self
-
end
-
-
1
alias_method :<<, :add
-
-
# Register a block as simple hash transformation that runs before expanding the pattern.
-
# @return [Mustermann::Expander] the expander
-
#
-
# @overload cast
-
# Register a block as simple hash transformation that runs before expanding the pattern for all entries.
-
#
-
# @example casting everything that implements to_param to param
-
# expander.cast { |o| o.to_param if o.respond_to? :to_param }
-
#
-
# @yield every key/value pair
-
# @yieldparam key [Symbol] omitted if block takes less than 2
-
# @yieldparam value [Object] omitted if block takes no arguments
-
# @yieldreturn [Hash{Symbol: Object}] will replace key/value pair with returned hash
-
# @yieldreturn [nil, false] will keep key/value pair in hash
-
# @yieldreturn [Object] will replace value with returned object
-
#
-
# @overload cast(*type_matchers)
-
# Register a block as simple hash transformation that runs before expanding the pattern for certain entries.
-
#
-
# @example convert user to user_id
-
# expander = Mustermann::Expander.new('/users/:user_id')
-
# expand.cast(:user) { |user| { user_id: user.id } }
-
#
-
# expand.expand(user: User.current) # => "/users/42"
-
#
-
# @example convert user, page, image to user_id, page_id, image_id
-
# expander = Mustermann::Expander.new('/users/:user_id', '/pages/:page_id', '/:image_id.jpg')
-
# expand.cast(:user, :page, :image) { |key, value| { "#{key}_id".to_sym => value.id } }
-
#
-
# expand.expand(user: User.current) # => "/users/42"
-
#
-
# @example casting to multiple key/value pairs
-
# expander = Mustermann::Expander.new('/users/:user_id/:image_id.:format')
-
# expander.cast(:image) { |i| { user_id: i.owner.id, image_id: i.id, format: i.format } }
-
#
-
# expander.expander(image: User.current.avatar) # => "/users/42/avatar.jpg"
-
#
-
# @example casting all ActiveRecord objects to param
-
# expander.cast(ActiveRecord::Base, &:to_param)
-
#
-
# @param [Array<Symbol, Regexp, #===>] type_matchers
-
# To identify key/value pairs to match against.
-
# Regexps and Symbols match against key, everything else matches against value.
-
#
-
# @yield every key/value pair
-
# @yieldparam key [Symbol] omitted if block takes less than 2
-
# @yieldparam value [Object] omitted if block takes no arguments
-
# @yieldreturn [Hash{Symbol: Object}] will replace key/value pair with returned hash
-
# @yieldreturn [nil, false] will keep key/value pair in hash
-
# @yieldreturn [Object] will replace value with returned object
-
#
-
# @overload cast(*cast_objects)
-
#
-
# @param [Array<#cast>] cast_objects
-
# Before expanding, will call #cast on these objects for each key/value pair.
-
# Return value will be treated same as block return values described above.
-
1
def cast(*types, &block)
-
caster.register(*types, &block)
-
self
-
end
-
-
# @example Expanding a pattern
-
# pattern = Mustermann::Expander.new('/:name', '/:name.:ext')
-
# pattern.expand(name: 'hello') # => "/hello"
-
# pattern.expand(name: 'hello', ext: 'png') # => "/hello.png"
-
#
-
# @example Handling additional values
-
# pattern = Mustermann::Expander.new('/:name', '/:name.:ext')
-
# pattern.expand(:ignore, name: 'hello', ext: 'png', scale: '2x') # => "/hello.png"
-
# pattern.expand(:append, name: 'hello', ext: 'png', scale: '2x') # => "/hello.png?scale=2x"
-
# pattern.expand(:raise, name: 'hello', ext: 'png', scale: '2x') # raises Mustermann::ExpandError
-
#
-
# @example Setting additional values behavior for the expander object
-
# pattern = Mustermann::Expander.new('/:name', '/:name.:ext', additional_values: :append)
-
# pattern.expand(name: 'hello', ext: 'png', scale: '2x') # => "/hello.png?scale=2x"
-
#
-
# @param [Symbol] behavior
-
# What to do with additional key/value pairs not present in the values hash.
-
# Possible options: :raise, :ignore, :append.
-
#
-
# @param [Hash{Symbol: #to_s, Array<#to_s>}] values
-
# Values to use for expansion.
-
#
-
# @return [String] expanded string
-
# @raise [NotImplementedError] raised if expand is not supported.
-
# @raise [Mustermann::ExpandError] raised if a value is missing or unknown
-
1
def expand(behavior = nil, values = {})
-
behavior, values = nil, behavior if behavior.is_a? Hash
-
values = map_values(values)
-
-
case behavior || additional_values
-
when :raise then @api_expander.expand(values)
-
when :ignore then with_rest(values) { |uri, rest| uri }
-
when :append then with_rest(values) { |uri, rest| append(uri, rest) }
-
else raise ArgumentError, "unknown behavior %p" % behavior
-
end
-
end
-
-
# @see Object#==
-
1
def ==(other)
-
return false unless other.class == self.class
-
other.patterns == patterns and other.additional_values == additional_values
-
end
-
-
# @see Object#eql?
-
1
def eql?(other)
-
return false unless other.class == self.class
-
other.patterns.eql? patterns and other.additional_values.eql? additional_values
-
end
-
-
# @see Object#hash
-
1
def hash
-
patterns.hash + additional_values.hash
-
end
-
-
1
def expandable?(values)
-
return false unless values
-
expandable, _ = split_values(map_values(values))
-
@api_expander.expandable? expandable
-
end
-
-
1
def with_rest(values)
-
expandable, non_expandable = split_values(values)
-
yield expand(:raise, slice(values, expandable)), slice(values, non_expandable)
-
end
-
-
1
def split_values(values)
-
expandable = @api_expander.expandable_keys(values.keys)
-
non_expandable = values.keys - expandable
-
[expandable, non_expandable]
-
end
-
-
1
def slice(hash, keys)
-
Hash[keys.map { |k| [k, hash[k]] }]
-
end
-
-
1
def append(uri, values)
-
return uri unless values and values.any?
-
entries = values.map { |pair| pair.map { |e| @api_expander.escape(e, also_escape: /[\/\?#\&\=%]/) }.join(?=) }
-
"#{ uri }#{ uri[??]??&:?? }#{ entries.join(?&) }"
-
end
-
-
1
def map_values(values)
-
values = values.dup
-
@api_expander.keys.each { |key| values[key] ||= values.delete(key.to_s) if values.include? key.to_s }
-
caster.cast(values).delete_if { |k, v| v.nil? }
-
end
-
-
1
private :with_rest, :slice, :append, :caster, :map_values, :split_values
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mustermann'
-
1
require 'mustermann/pattern'
-
1
require 'mustermann/ast/node'
-
-
1
module Mustermann
-
# Matches strings that are identical to the pattern.
-
#
-
# @example
-
# Mustermann.new('/:foo', type: :identity) === '/bar' # => false
-
#
-
# @see Mustermann::Pattern
-
# @see file:README.md#identity Syntax description in the README
-
1
class Identity < Pattern
-
1
include Concat::Native
-
1
register :identity
-
-
# @param (see Mustermann::Pattern#===)
-
# @return (see Mustermann::Pattern#===)
-
# @see (see Mustermann::Pattern#===)
-
1
def ===(string)
-
unescape(string) == @string
-
end
-
-
# @param (see Mustermann::Pattern#peek_size)
-
# @return (see Mustermann::Pattern#peek_size)
-
# @see (see Mustermann::Pattern#peek_size)
-
1
def peek_size(string)
-
return unless unescape(string).start_with? @string
-
return @string.size if string.start_with? @string # optimization
-
@string.each_char.with_index.inject(0) do |count, (char, index)|
-
char_size = 1
-
escaped = @@uri.escape(char, /./)
-
char_size = escaped.size if string[index, escaped.size].downcase == escaped.downcase
-
count + char_size
-
end
-
end
-
-
# URI templates support generating templates (the logic is quite complex, though).
-
#
-
# @example (see Mustermann::Pattern#to_templates)
-
# @param (see Mustermann::Pattern#to_templates)
-
# @return (see Mustermann::Pattern#to_templates)
-
# @see Mustermann::Pattern#to_templates
-
1
def to_templates
-
[@@uri.escape(to_s)]
-
end
-
-
# Generates an AST so it's compatible with {Mustermann::AST::Pattern}.
-
# Not used internally by {Mustermann::Identity}.
-
# @!visibility private
-
1
def to_ast
-
payload = @string.each_char.with_index.map { |c, i| AST::Node[c == ?/ ? :separator : :char].new(c, start: i, stop: i+1) }
-
AST::Node[:root].new(payload, pattern: @string, start: 0, stop: @string.length)
-
end
-
-
# Identity patterns support expanding.
-
#
-
# This implementation does not use {Mustermann::Expander} internally to save memory and
-
# compilation time.
-
#
-
# @example (see Mustermann::Pattern#expand)
-
# @param (see Mustermann::Pattern#expand)
-
# @return (see Mustermann::Pattern#expand)
-
# @raise (see Mustermann::Pattern#expand)
-
# @see Mustermann::Pattern#expand
-
# @see Mustermann::Expander
-
1
def expand(behavior = nil, values = {})
-
return to_s if values.empty? or behavior == :ignore
-
raise ExpandError, "cannot expand with keys %p" % values.keys.sort if behavior == :raise
-
raise ArgumentError, "unknown behavior %p" % behavior if behavior != :append
-
params = values.map { |key, value| @@uri.escape(key.to_s) + "=" + @@uri.escape(value.to_s, /[^\w]/) }
-
separator = @string.include?(??) ? ?& : ??
-
@string + separator + params.join(?&)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mustermann/error'
-
1
require 'mustermann/simple_match'
-
1
require 'mustermann/equality_map'
-
1
require 'uri'
-
-
1
module Mustermann
-
# Superclass for all pattern implementations.
-
# @abstract
-
1
class Pattern
-
1
include Mustermann
-
1
@@uri ||= URI::Parser.new
-
-
# List of supported options.
-
#
-
# @overload supported_options
-
# @return [Array<Symbol>] list of supported options
-
# @overload supported_options(*list)
-
# Adds options to the list.
-
#
-
# @api private
-
# @param [Symbol] *list adds options to the list of supported options
-
# @return [Array<Symbol>] list of supported options
-
1
def self.supported_options(*list)
-
9
@supported_options ||= []
-
9
options = @supported_options.concat(list)
-
9
options += superclass.supported_options if self < Pattern
-
9
options
-
end
-
-
# Registers the pattern with Mustermann.
-
# @see Mustermann.register
-
# @!visibility private
-
1
def self.register(*names)
-
7
names.each { |name| Mustermann.register(name, self) }
-
end
-
-
# @param [Symbol] option The option to check.
-
# @return [Boolean] Whether or not option is supported.
-
1
def self.supported?(option, **options)
-
supported_options.include? option
-
end
-
-
# @overload new(string, **options)
-
# @param (see #initialize)
-
# @raise (see #initialize)
-
# @raise [ArgumentError] if some option is not supported
-
# @return [Mustermann::Pattern] a new instance of Mustermann::Pattern
-
# @see #initialize
-
1
def self.new(string, ignore_unknown_options: false, **options)
-
20
if ignore_unknown_options
-
options = options.select { |key, value| supported?(key, **options) if key != :ignore_unknown_options }
-
else
-
20
unsupported = options.keys.detect { |key| not supported?(key, **options) }
-
20
raise ArgumentError, "unsupported option %p for %p" % [unsupported, self] if unsupported
-
end
-
-
20
@map ||= EqualityMap.new
-
36
@map.fetch([string, options]) { super(string, **options) { options } }
-
end
-
-
1
supported_options :uri_decode, :ignore_unknown_options
-
1
attr_reader :uri_decode
-
-
# options hash passed to new (with unsupported options removed)
-
# @!visibility private
-
1
attr_reader :options
-
-
# @overload initialize(string, **options)
-
# @param [String] string the string representation of the pattern
-
# @param [Hash] options options for fine-tuning the pattern behavior
-
# @raise [Mustermann::Error] if the pattern can't be generated from the string
-
# @see file:README.md#Types_and_Options "Types and Options" in the README
-
# @see Mustermann.new
-
1
def initialize(string, uri_decode: true, **options)
-
8
@uri_decode = uri_decode
-
8
@string = string.to_s.dup
-
8
@options = yield.freeze if block_given?
-
end
-
-
# @return [String] the string representation of the pattern
-
1
def to_s
-
2
@string.dup
-
end
-
-
# @param [String] string The string to match against
-
# @return [MatchData, Mustermann::SimpleMatch, nil] MatchData or similar object if the pattern matches.
-
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-match Regexp#match
-
# @see http://ruby-doc.org/core-2.0/MatchData.html MatchData
-
# @see Mustermann::SimpleMatch
-
1
def match(string)
-
SimpleMatch.new(string) if self === string
-
end
-
-
# @param [String] string The string to match against
-
# @return [Integer, nil] nil if pattern does not match the string, zero if it does.
-
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-3D-7E Regexp#=~
-
1
def =~(string)
-
0 if self === string
-
end
-
-
# @param [String] string The string to match against
-
# @return [Boolean] Whether or not the pattern matches the given string
-
# @note Needs to be overridden by subclass.
-
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-3D-3D-3D Regexp#===
-
1
def ===(string)
-
raise NotImplementedError, 'subclass responsibility'
-
end
-
-
# Used by Ruby internally for hashing.
-
# @return [Integer] same has value for patterns that are equal
-
1
def hash
-
self.class.hash | @string.hash | options.hash
-
end
-
-
# Two patterns are considered equal if they are of the same type, have the same pattern string
-
# and the same options.
-
# @return [true, false]
-
1
def ==(other)
-
other.class == self.class and other.to_s == @string and other.options == options
-
end
-
-
# Two patterns are considered equal if they are of the same type, have the same pattern string
-
# and the same options.
-
# @return [true, false]
-
1
def eql?(other)
-
other.class.eql?(self.class) and other.to_s.eql?(@string) and other.options.eql?(options)
-
end
-
-
# Tries to match the pattern against the beginning of the string (as opposed to the full string).
-
# Will return the count of the matching characters if it matches.
-
#
-
# @example
-
# pattern = Mustermann.new('/:name')
-
# pattern.size("/Frank/Sinatra") # => 6
-
#
-
# @param [String] string The string to match against
-
# @return [Integer, nil] the number of characters that match
-
1
def peek_size(string)
-
# this is a very naive, unperformant implementation
-
string.size.downto(0).detect { |s| self === string[0, s] }
-
end
-
-
# Tries to match the pattern against the beginning of the string (as opposed to the full string).
-
# Will return the substring if it matches.
-
#
-
# @example
-
# pattern = Mustermann.new('/:name')
-
# pattern.peek("/Frank/Sinatra") # => "/Frank"
-
#
-
# @param [String] string The string to match against
-
# @return [String, nil] matched subsctring
-
1
def peek(string)
-
size = peek_size(string)
-
string[0, size] if size
-
end
-
-
# Tries to match the pattern against the beginning of the string (as opposed to the full string).
-
# Will return a MatchData or similar instance for the matched substring.
-
#
-
# @example
-
# pattern = Mustermann.new('/:name')
-
# pattern.peek("/Frank/Sinatra") # => #<MatchData "/Frank" name:"Frank">
-
#
-
# @param [String] string The string to match against
-
# @return [MatchData, Mustermann::SimpleMatch, nil] MatchData or similar object if the pattern matches.
-
# @see #peek_params
-
1
def peek_match(string)
-
matched = peek(string)
-
match(matched) if matched
-
end
-
-
# Tries to match the pattern against the beginning of the string (as opposed to the full string).
-
# Will return a two element Array with the params parsed from the substring as first entry and the length of
-
# the substring as second.
-
#
-
# @example
-
# pattern = Mustermann.new('/:name')
-
# params, _ = pattern.peek_params("/Frank/Sinatra")
-
#
-
# puts "Hello, #{params['name']}!" # Hello, Frank!
-
#
-
# @param [String] string The string to match against
-
# @return [Array<Hash, Integer>, nil] Array with params hash and length of substing if matched, nil otherwise
-
1
def peek_params(string)
-
match = peek_match(string)
-
[params(captures: match), match.to_s.size] if match
-
end
-
-
# @return [Hash{String: Array<Integer>}] capture names mapped to capture index.
-
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-named_captures Regexp#named_captures
-
1
def named_captures
-
{}
-
end
-
-
# @return [Array<String>] capture names.
-
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-names Regexp#names
-
1
def names
-
[]
-
end
-
-
# @param [String] string the string to match against
-
# @return [Hash{String: String, Array<String>}, nil] Sinatra style params if pattern matches.
-
1
def params(string = nil, captures: nil, offset: 0)
-
15
return unless captures ||= match(string)
-
6
params = named_captures.map do |name, positions|
-
values = positions.map { |pos| map_param(name, captures[pos + offset]) }.flatten
-
values = values.first if values.size < 2 and not always_array? name
-
[name, values]
-
end
-
-
6
Hash[params]
-
end
-
-
# @note This method is only implemented by certain subclasses.
-
#
-
# @example Expanding a pattern
-
# pattern = Mustermann.new('/:name(.:ext)?')
-
# pattern.expand(name: 'hello') # => "/hello"
-
# pattern.expand(name: 'hello', ext: 'png') # => "/hello.png"
-
#
-
# @example Checking if a pattern supports expanding
-
# if pattern.respond_to? :expand
-
# pattern.expand(name: "foo")
-
# else
-
# warn "does not support expanding"
-
# end
-
#
-
# Expanding is supported by almost all patterns (notable exceptions are {Mustermann::Shell},
-
# {Mustermann::Regular} and {Mustermann::Simple}).
-
#
-
# Union {Mustermann::Composite} patterns (with the | operator) support expanding if all
-
# patterns they are composed of also support it.
-
#
-
# @param (see Mustermann::Expander#expand)
-
# @return [String] expanded string
-
# @raise [NotImplementedError] raised if expand is not supported.
-
# @raise [Mustermann::ExpandError] raised if a value is missing or unknown
-
# @see Mustermann::Expander
-
1
def expand(behavior = nil, values = {})
-
raise NotImplementedError, "expanding not supported by #{self.class}"
-
end
-
-
# @note This method is only implemented by certain subclasses.
-
#
-
# Generates a list of URI template strings representing the pattern.
-
#
-
# Note that this transformation is lossy and the strings matching these
-
# templates might not match the pattern (and vice versa).
-
#
-
# This comes in quite handy since URI templates are not made for pattern matching.
-
# That way you can easily use a more precise template syntax and have it automatically
-
# generate hypermedia links for you.
-
#
-
# @example generating templates
-
# Mustermann.new("/:name").to_templates # => ["/{name}"]
-
# Mustermann.new("/:foo(@:bar)?/*baz").to_templates # => ["/{foo}@{bar}/{+baz}", "/{foo}/{+baz}"]
-
# Mustermann.new("/{name}", type: :template).to_templates # => ["/{name}"]
-
#
-
# @example generating templates from composite patterns
-
# pattern = Mustermann.new('/:name')
-
# pattern |= Mustermann.new('/{name}', type: :template)
-
# pattern |= Mustermann.new('/example/*nested')
-
# pattern.to_templates # => ["/{name}", "/example/{+nested}"]
-
#
-
# Template generation is supported by almost all patterns (notable exceptions are
-
# {Mustermann::Shell}, {Mustermann::Regular} and {Mustermann::Simple}).
-
# Union {Mustermann::Composite} patterns (with the | operator) support template generation
-
# if all patterns they are composed of also support it.
-
#
-
# @example Checking if a pattern supports expanding
-
# if pattern.respond_to? :to_templates
-
# pattern.to_templates
-
# else
-
# warn "does not support template generation"
-
# end
-
#
-
# @return [Array<String>] list of URI templates
-
1
def to_templates
-
raise NotImplementedError, "template generation not supported by #{self.class}"
-
end
-
-
# @overload |(other)
-
# Creates a pattern that matches any string matching either one of the patterns.
-
# If a string is supplied, it is treated as an identity pattern.
-
#
-
# @example
-
# pattern = Mustermann.new('/foo/:name') | Mustermann.new('/:first/:second')
-
# pattern === '/foo/bar' # => true
-
# pattern === '/fox/bar' # => true
-
# pattern === '/foo' # => false
-
#
-
# @overload &(other)
-
# Creates a pattern that matches any string matching both of the patterns.
-
# If a string is supplied, it is treated as an identity pattern.
-
#
-
# @example
-
# pattern = Mustermann.new('/foo/:name') & Mustermann.new('/:first/:second')
-
# pattern === '/foo/bar' # => true
-
# pattern === '/fox/bar' # => false
-
# pattern === '/foo' # => false
-
#
-
# @overload ^(other)
-
# Creates a pattern that matches any string matching exactly one of the patterns.
-
# If a string is supplied, it is treated as an identity pattern.
-
#
-
# @example
-
# pattern = Mustermann.new('/foo/:name') ^ Mustermann.new('/:first/:second')
-
# pattern === '/foo/bar' # => false
-
# pattern === '/fox/bar' # => true
-
# pattern === '/foo' # => false
-
#
-
# @param [Mustermann::Pattern, String] other the other pattern
-
# @return [Mustermann::Pattern] a composite pattern
-
1
def |(other)
-
Mustermann::Composite.new(self, other, operator: __callee__, type: :identity)
-
end
-
-
1
alias_method :&, :|
-
1
alias_method :^, :|
-
-
# @example
-
# require 'mustermann'
-
# prefix = Mustermann.new("/:prefix")
-
# about = prefix + "/about"
-
# about.params("/main/about") # => {"prefix" => "main"}
-
#
-
# Creates a concatenated pattern by combingin self with the other pattern supplied.
-
# Patterns of different types can be mixed. The availability of `to_templates` and
-
# `expand` depends on the patterns being concatenated.
-
#
-
# String input is treated as identity pattern.
-
#
-
# @param [Mustermann::Pattern, String] other pattern to be appended
-
# @return [Mustermann::Pattern] concatenated pattern
-
1
def +(other)
-
Concat.new(self, other, type: :identity)
-
end
-
-
# @example
-
# pattern = Mustermann.new('/:a/:b')
-
# strings = ["foo/bar", "/foo/bar", "/foo/bar/"]
-
# strings.detect(&pattern) # => "/foo/bar"
-
#
-
# @return [Proc] proc wrapping {#===}
-
1
def to_proc
-
@to_proc ||= method(:===).to_proc
-
end
-
-
# @!visibility private
-
# @return [Boolean]
-
# @see Object#respond_to?
-
1
def respond_to?(method, *args)
-
2
return super unless %i[expand to_templates].include? method
-
respond_to_special?(method)
-
end
-
-
# @!visibility private
-
# @return [Boolean]
-
# @see #respond_to?
-
1
def respond_to_special?(method)
-
method(method).owner != Mustermann::Pattern
-
end
-
-
# @!visibility private
-
1
def inspect
-
"#<%p:%p>" % [self.class, @string]
-
end
-
-
# @!visibility private
-
1
def simple_inspect
-
type = self.class.name[/[^:]+$/].downcase
-
"%s:%p" % [type, @string]
-
end
-
-
# @!visibility private
-
1
def map_param(key, value)
-
unescape(value, true)
-
end
-
-
# @!visibility private
-
1
def unescape(string, decode = uri_decode)
-
return string unless decode and string
-
@@uri.unescape(string)
-
end
-
-
# @!visibility private
-
1
ALWAYS_ARRAY = %w[splat captures]
-
-
# @!visibility private
-
1
def always_array?(key)
-
ALWAYS_ARRAY.include? key
-
end
-
-
1
private :unescape, :map_param, :respond_to_special?
-
1
private_constant :ALWAYS_ARRAY
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mustermann/pattern'
-
1
require 'forwardable'
-
-
1
module Mustermann
-
# Superclass for patterns that internally compile to a regular expression.
-
# @see Mustermann::Pattern
-
# @abstract
-
1
class RegexpBased < Pattern
-
# @return [Regexp] regular expression equivalent to the pattern.
-
1
attr_reader :regexp
-
1
alias_method :to_regexp, :regexp
-
-
# @param (see Mustermann::Pattern#initialize)
-
# @return (see Mustermann::Pattern#initialize)
-
# @see (see Mustermann::Pattern#initialize)
-
1
def initialize(string, **options)
-
8
super
-
8
regexp = compile(**options)
-
8
@peek_regexp = /\A#{regexp}/
-
8
@regexp = /\A#{regexp}\Z/
-
end
-
-
# @param (see Mustermann::Pattern#peek_size)
-
# @return (see Mustermann::Pattern#peek_size)
-
# @see (see Mustermann::Pattern#peek_size)
-
1
def peek_size(string)
-
return unless match = peek_match(string)
-
match.to_s.size
-
end
-
-
# @param (see Mustermann::Pattern#peek_match)
-
# @return (see Mustermann::Pattern#peek_match)
-
# @see (see Mustermann::Pattern#peek_match)
-
1
def peek_match(string)
-
@peek_regexp.match(string)
-
end
-
-
1
extend Forwardable
-
1
def_delegators :regexp, :===, :=~, :match, :names, :named_captures
-
-
1
def compile(**options)
-
raise NotImplementedError, 'subclass responsibility'
-
end
-
-
1
private :compile
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mustermann'
-
1
require 'mustermann/regexp_based'
-
1
require 'strscan'
-
-
1
module Mustermann
-
# Regexp pattern implementation.
-
#
-
# @example
-
# Mustermann.new('/.*', type: :regexp) === '/bar' # => true
-
#
-
# @see Mustermann::Pattern
-
# @see file:README.md#simple Syntax description in the README
-
1
class Regular < RegexpBased
-
1
include Concat::Native
-
1
register :regexp, :regular
-
1
supported_options :check_anchors
-
-
# @param (see Mustermann::Pattern#initialize)
-
# @return (see Mustermann::Pattern#initialize)
-
# @see (see Mustermann::Pattern#initialize)
-
1
def initialize(string, check_anchors: true, **options)
-
1
string = $1 if string.to_s =~ /\A\(\?\-mix\:(.*)\)\Z/ && string.inspect == "/#$1/"
-
1
string = string.source.gsub!(/(?<!\\)(?:\s|#.*$)/, '') if extended_regexp?(string)
-
1
@check_anchors = check_anchors
-
1
super(string, **options)
-
end
-
-
1
def compile(**options)
-
1
if @check_anchors
-
1
scanner = ::StringScanner.new(@string)
-
1
check_anchors(scanner) until scanner.eos?
-
end
-
-
1
/#{@string}/
-
end
-
-
1
def check_anchors(scanner)
-
2
return scanner.scan_until(/\]/) if scanner.scan(/\[/)
-
2
return scanner.scan(/\\?./) unless illegal = scanner.scan(/\\[AzZ]|[\^\$]/)
-
raise CompileError, "regular expression should not contain %s: %p" % [illegal.to_s, @string]
-
end
-
-
1
def extended_regexp?(string)
-
1
not (Regexp.new(string).options & Regexp::EXTENDED).zero?
-
end
-
-
1
private :compile, :check_anchors, :extended_regexp?
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mustermann
-
# Fakes MatchData for patterns that do not support capturing.
-
# @see http://ruby-doc.org/core-2.0/MatchData.html MatchData
-
1
class SimpleMatch
-
# @api private
-
1
def initialize(string = "", names: [], captures: [])
-
@string = string.dup
-
@names = names
-
@captures = captures
-
end
-
-
# @return [String] the string that was matched against
-
1
def to_s
-
@string.dup
-
end
-
-
# @return [Array<String>] empty array for imitating MatchData interface
-
1
def names
-
@names.dup
-
end
-
-
# @return [Array<String>] empty array for imitating MatchData interface
-
1
def captures
-
@captures.dup
-
end
-
-
# @return [nil] imitates MatchData interface
-
1
def [](*args)
-
args.map! do |arg|
-
next arg unless arg.is_a? Symbol or arg.is_a? String
-
names.index(arg.to_s)
-
end
-
@captures[*args]
-
end
-
-
# @!visibility private
-
1
def +(other)
-
SimpleMatch.new(@string + other.to_s,
-
names: @names + other.names,
-
captures: @captures + other.captures)
-
end
-
-
# @return [String] string representation
-
1
def inspect
-
"#<%p %p>" % [self.class, @string]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'mustermann'
-
1
require 'mustermann/identity'
-
1
require 'mustermann/ast/pattern'
-
1
require 'mustermann/sinatra/parser'
-
1
require 'mustermann/sinatra/safe_renderer'
-
1
require 'mustermann/sinatra/try_convert'
-
-
1
module Mustermann
-
# Sinatra 2.0 style pattern implementation.
-
#
-
# @example
-
# Mustermann.new('/:foo') === '/bar' # => true
-
#
-
# @see Mustermann::Pattern
-
# @see file:README.md#sinatra Syntax description in the README
-
1
class Sinatra < AST::Pattern
-
1
include Concat::Native
-
1
register :sinatra
-
-
# Takes a string and espaces any characters that have special meaning for Sinatra patterns.
-
#
-
# @example
-
# require 'mustermann/sinatra'
-
# Mustermann::Sinatra.escape("/:name") # => "/\\:name"
-
#
-
# @param [#to_s] string the input string
-
# @return [String] the escaped string
-
1
def self.escape(string)
-
string.to_s.gsub(/[\?\(\)\*:\\\|\{\}]/) { |c| "\\#{c}" }
-
end
-
-
# Tries to convert the given input object to a Sinatra pattern with the given options, without
-
# changing its parsing semantics.
-
# @return [Mustermann::Sinatra, nil] the converted pattern, if possible
-
# @!visibility private
-
1
def self.try_convert(input, **options)
-
TryConvert.convert(input, **options)
-
end
-
-
# Creates a pattern that matches any string matching either one of the patterns.
-
# If a string is supplied, it is treated as a fully escaped Sinatra pattern.
-
#
-
# If the other pattern is also a Sintara pattern, it might join the two to a third
-
# sinatra pattern instead of generating a composite for efficiency reasons.
-
#
-
# This only happens if the sinatra pattern behaves exactly the same as a composite
-
# would in regards to matching, parsing, expanding and template generation.
-
#
-
# @example
-
# pattern = Mustermann.new('/foo/:name') | Mustermann.new('/:first/:second')
-
# pattern === '/foo/bar' # => true
-
# pattern === '/fox/bar' # => true
-
# pattern === '/foo' # => false
-
#
-
# @param [Mustermann::Pattern, String] other the other pattern
-
# @return [Mustermann::Pattern] a composite pattern
-
# @see Mustermann::Pattern#|
-
1
def |(other)
-
return super unless converted = self.class.try_convert(other, **options)
-
return super unless converted.names.empty? or names.empty?
-
self.class.new(safe_string + "|" + converted.safe_string, **options)
-
end
-
-
# Generates a string represenation of the pattern that can safely be used for def interpolation
-
# without changing its semantics.
-
#
-
# @example
-
# require 'mustermann'
-
# unsafe = Mustermann.new("/:name")
-
#
-
# Mustermann.new("#{unsafe}bar").params("/foobar") # => { "namebar" => "foobar" }
-
# Mustermann.new("#{unsafe.safe_string}bar").params("/foobar") # => { "name" => "bar" }
-
#
-
# @return [String] string representatin of the pattern
-
1
def safe_string
-
@safe_string ||= SafeRenderer.translate(to_ast)
-
end
-
-
# @!visibility private
-
1
def native_concat(other)
-
return unless converted = self.class.try_convert(other, **options)
-
safe_string + converted.safe_string
-
end
-
-
1
private :native_concat
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mustermann
-
1
class Sinatra < AST::Pattern
-
# Sinatra syntax definition.
-
# @!visibility private
-
1
class Parser < AST::Parser
-
1
on(nil, ??, ?)) { |c| unexpected(c) }
-
-
1
on(?*) { |c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) }
-
4
on(?:) { |c| node(:capture) { scan(/\w+/) } }
-
1
on(?\\) { |c| node(:char, expect(/./)) }
-
1
on(?() { |c| node(:group) { read unless scan(?)) } }
-
1
on(?|) { |c| node(:or) }
-
-
1
on ?{ do |char|
-
current_pos = buffer.pos
-
type = scan(?+) ? :named_splat : :capture
-
name = expect(/[\w\.]+/)
-
if type == :capture && scan(?|)
-
buffer.pos = current_pos
-
capture = proc do
-
start = pos
-
match = expect(/(?<capture>[^\|}]+)/)
-
node(:capture, match[:capture], start: start)
-
end
-
grouped_captures = node(:group, [capture[]]) do
-
if scan(?|)
-
[min_size(pos - 1, pos, node(:or)), capture[]]
-
end
-
end
-
grouped_captures if expect(?})
-
else
-
type = :splat if type == :named_splat and name == 'splat'
-
expect(?})
-
node(type, name)
-
end
-
end
-
-
1
suffix ?? do |char, element|
-
2
node(:optional, element)
-
end
-
end
-
-
1
private_constant :Parser
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mustermann
-
1
class Sinatra < AST::Pattern
-
# Generates a string that can safely be concatenated with other strings
-
# without chaning its semantics
-
# @see #safe_string
-
# @!visibility private
-
1
SafeRenderer = AST::Translator.create do
-
1
translate(:splat, :named_splat) { "{+#{name}}" }
-
1
translate(:char, :separator) { Sinatra.escape(payload) }
-
1
translate(:root) { t(payload) }
-
1
translate(:group) { "(#{t(payload)})" }
-
1
translate(:union) { "(#{t(payload, join: ?|)})" }
-
1
translate(:optional) { "#{t(payload)}?" }
-
1
translate(:with_look_ahead) { t([head, payload]) }
-
1
translate(Array) { |join: ""| map { |e| t(e) }.join(join) }
-
-
1
translate(:capture) do
-
raise Mustermann::Error, 'cannot render variables' if node.is_a? :variable
-
raise Mustermann::Error, 'cannot translate constraints' if constraint or qualifier or convert
-
prefix = node.is_a?(:splat) ? "+" : ""
-
"{#{prefix}#{name}}"
-
end
-
end
-
-
1
private_constant :SafeRenderer
-
end
-
end
-
# frozen_string_literal: true
-
1
module Mustermann
-
1
class Sinatra < AST::Pattern
-
# Tries to translate objects to Sinatra patterns.
-
# @!visibility private
-
1
class TryConvert < AST::Translator
-
# @return [Mustermann::Sinatra, nil]
-
# @!visibility private
-
1
def self.convert(input, **options)
-
new(options).translate(input)
-
end
-
-
# Expected options for the resulting pattern.
-
# @!visibility private
-
1
attr_reader :options
-
-
# @!visibility private
-
1
def initialize(options)
-
@options = options
-
end
-
-
# @return [Mustermann::Sinatra]
-
# @!visibility private
-
1
def new(input, escape = false)
-
input = Mustermann::Sinatra.escape(input) if escape
-
Mustermann::Sinatra.new(input, **options)
-
end
-
-
# @return [true, false] whether or not expected pattern should have uri_decode option set
-
# @!visibility private
-
1
def uri_decode
-
options.fetch(:uri_decode, true)
-
end
-
-
1
translate(Object) { nil }
-
1
translate(String) { t.new(self, true) }
-
-
1
translate(Identity) { t.new(self, true) if uri_decode == t.uri_decode }
-
1
translate(Sinatra) { node if options == t.options }
-
-
1
translate AST::Pattern do
-
next unless options == t.options
-
t.new(SafeRenderer.translate(to_ast)) rescue nil
-
end
-
end
-
-
1
private_constant :TryConvert
-
end
-
end
-
# frozen_string_literal: true
-
-
# Copyright (C) 2007-2019 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
-
#
-
# Rack is freely distributable under the terms of an MIT-style license.
-
# See MIT-LICENSE or https://opensource.org/licenses/MIT.
-
-
# The Rack main module, serving as a namespace for all core Rack
-
# modules and classes.
-
#
-
# All modules meant for use in your application are <tt>autoload</tt>ed here,
-
# so it should be enough just to <tt>require 'rack'</tt> in your code.
-
-
1
require_relative 'rack/version'
-
-
1
module Rack
-
1
HTTP_HOST = 'HTTP_HOST'
-
1
HTTP_PORT = 'HTTP_PORT'
-
1
HTTP_VERSION = 'HTTP_VERSION'
-
1
HTTPS = 'HTTPS'
-
1
PATH_INFO = 'PATH_INFO'
-
1
REQUEST_METHOD = 'REQUEST_METHOD'
-
1
REQUEST_PATH = 'REQUEST_PATH'
-
1
SCRIPT_NAME = 'SCRIPT_NAME'
-
1
QUERY_STRING = 'QUERY_STRING'
-
1
SERVER_PROTOCOL = 'SERVER_PROTOCOL'
-
1
SERVER_NAME = 'SERVER_NAME'
-
1
SERVER_PORT = 'SERVER_PORT'
-
1
CACHE_CONTROL = 'Cache-Control'
-
1
EXPIRES = 'Expires'
-
1
CONTENT_LENGTH = 'Content-Length'
-
1
CONTENT_TYPE = 'Content-Type'
-
1
SET_COOKIE = 'Set-Cookie'
-
1
TRANSFER_ENCODING = 'Transfer-Encoding'
-
1
HTTP_COOKIE = 'HTTP_COOKIE'
-
1
ETAG = 'ETag'
-
-
# HTTP method verbs
-
1
GET = 'GET'
-
1
POST = 'POST'
-
1
PUT = 'PUT'
-
1
PATCH = 'PATCH'
-
1
DELETE = 'DELETE'
-
1
HEAD = 'HEAD'
-
1
OPTIONS = 'OPTIONS'
-
1
LINK = 'LINK'
-
1
UNLINK = 'UNLINK'
-
1
TRACE = 'TRACE'
-
-
# Rack environment variables
-
1
RACK_VERSION = 'rack.version'
-
1
RACK_TEMPFILES = 'rack.tempfiles'
-
1
RACK_ERRORS = 'rack.errors'
-
1
RACK_LOGGER = 'rack.logger'
-
1
RACK_INPUT = 'rack.input'
-
1
RACK_SESSION = 'rack.session'
-
1
RACK_SESSION_OPTIONS = 'rack.session.options'
-
1
RACK_SHOWSTATUS_DETAIL = 'rack.showstatus.detail'
-
1
RACK_MULTITHREAD = 'rack.multithread'
-
1
RACK_MULTIPROCESS = 'rack.multiprocess'
-
1
RACK_RUNONCE = 'rack.run_once'
-
1
RACK_URL_SCHEME = 'rack.url_scheme'
-
1
RACK_HIJACK = 'rack.hijack'
-
1
RACK_IS_HIJACK = 'rack.hijack?'
-
1
RACK_HIJACK_IO = 'rack.hijack_io'
-
1
RACK_RECURSIVE_INCLUDE = 'rack.recursive.include'
-
1
RACK_MULTIPART_BUFFER_SIZE = 'rack.multipart.buffer_size'
-
1
RACK_MULTIPART_TEMPFILE_FACTORY = 'rack.multipart.tempfile_factory'
-
1
RACK_REQUEST_FORM_INPUT = 'rack.request.form_input'
-
1
RACK_REQUEST_FORM_HASH = 'rack.request.form_hash'
-
1
RACK_REQUEST_FORM_VARS = 'rack.request.form_vars'
-
1
RACK_REQUEST_COOKIE_HASH = 'rack.request.cookie_hash'
-
1
RACK_REQUEST_COOKIE_STRING = 'rack.request.cookie_string'
-
1
RACK_REQUEST_QUERY_HASH = 'rack.request.query_hash'
-
1
RACK_REQUEST_QUERY_STRING = 'rack.request.query_string'
-
1
RACK_METHODOVERRIDE_ORIGINAL_METHOD = 'rack.methodoverride.original_method'
-
1
RACK_SESSION_UNPACKED_COOKIE_DATA = 'rack.session.unpacked_cookie_data'
-
-
1
autoload :Builder, "rack/builder"
-
1
autoload :BodyProxy, "rack/body_proxy"
-
1
autoload :Cascade, "rack/cascade"
-
1
autoload :Chunked, "rack/chunked"
-
1
autoload :CommonLogger, "rack/common_logger"
-
1
autoload :ConditionalGet, "rack/conditional_get"
-
1
autoload :Config, "rack/config"
-
1
autoload :ContentLength, "rack/content_length"
-
1
autoload :ContentType, "rack/content_type"
-
1
autoload :ETag, "rack/etag"
-
1
autoload :Events, "rack/events"
-
1
autoload :File, "rack/file"
-
1
autoload :Files, "rack/files"
-
1
autoload :Deflater, "rack/deflater"
-
1
autoload :Directory, "rack/directory"
-
1
autoload :ForwardRequest, "rack/recursive"
-
1
autoload :Handler, "rack/handler"
-
1
autoload :Head, "rack/head"
-
1
autoload :Lint, "rack/lint"
-
1
autoload :Lock, "rack/lock"
-
1
autoload :Logger, "rack/logger"
-
1
autoload :MediaType, "rack/media_type"
-
1
autoload :MethodOverride, "rack/method_override"
-
1
autoload :Mime, "rack/mime"
-
1
autoload :NullLogger, "rack/null_logger"
-
1
autoload :Recursive, "rack/recursive"
-
1
autoload :Reloader, "rack/reloader"
-
1
autoload :RewindableInput, "rack/rewindable_input"
-
1
autoload :Runtime, "rack/runtime"
-
1
autoload :Sendfile, "rack/sendfile"
-
1
autoload :Server, "rack/server"
-
1
autoload :ShowExceptions, "rack/show_exceptions"
-
1
autoload :ShowStatus, "rack/show_status"
-
1
autoload :Static, "rack/static"
-
1
autoload :TempfileReaper, "rack/tempfile_reaper"
-
1
autoload :URLMap, "rack/urlmap"
-
1
autoload :Utils, "rack/utils"
-
1
autoload :Multipart, "rack/multipart"
-
-
1
autoload :MockRequest, "rack/mock"
-
1
autoload :MockResponse, "rack/mock"
-
-
1
autoload :Request, "rack/request"
-
1
autoload :Response, "rack/response"
-
-
1
module Auth
-
1
autoload :Basic, "rack/auth/basic"
-
1
autoload :AbstractRequest, "rack/auth/abstract/request"
-
1
autoload :AbstractHandler, "rack/auth/abstract/handler"
-
1
module Digest
-
1
autoload :MD5, "rack/auth/digest/md5"
-
1
autoload :Nonce, "rack/auth/digest/nonce"
-
1
autoload :Params, "rack/auth/digest/params"
-
1
autoload :Request, "rack/auth/digest/request"
-
end
-
end
-
-
1
module Session
-
1
autoload :Cookie, "rack/session/cookie"
-
1
autoload :Pool, "rack/session/pool"
-
1
autoload :Memcache, "rack/session/memcache"
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Rack
-
# Rack::Builder implements a small DSL to iteratively construct Rack
-
# applications.
-
#
-
# Example:
-
#
-
# require 'rack/lobster'
-
# app = Rack::Builder.new do
-
# use Rack::CommonLogger
-
# use Rack::ShowExceptions
-
# map "/lobster" do
-
# use Rack::Lint
-
# run Rack::Lobster.new
-
# end
-
# end
-
#
-
# run app
-
#
-
# Or
-
#
-
# app = Rack::Builder.app do
-
# use Rack::CommonLogger
-
# run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
-
# end
-
#
-
# run app
-
#
-
# +use+ adds middleware to the stack, +run+ dispatches to an application.
-
# You can use +map+ to construct a Rack::URLMap in a convenient way.
-
-
1
class Builder
-
-
# https://stackoverflow.com/questions/2223882/whats-the-difference-between-utf-8-and-utf-8-without-bom
-
1
UTF_8_BOM = '\xef\xbb\xbf'
-
-
# Parse the given config file to get a Rack application.
-
#
-
# If the config file ends in +.ru+, it is treated as a
-
# rackup file and the contents will be treated as if
-
# specified inside a Rack::Builder block, using the given
-
# options.
-
#
-
# If the config file does not end in +.ru+, it is
-
# required and Rack will use the basename of the file
-
# to guess which constant will be the Rack application to run.
-
# The options given will be ignored in this case.
-
#
-
# Examples:
-
#
-
# Rack::Builder.parse_file('config.ru')
-
# # Rack application built using Rack::Builder.new
-
#
-
# Rack::Builder.parse_file('app.rb')
-
# # requires app.rb, which can be anywhere in Ruby's
-
# # load path. After requiring, assumes App constant
-
# # contains Rack application
-
#
-
# Rack::Builder.parse_file('./my_app.rb')
-
# # requires ./my_app.rb, which should be in the
-
# # process's current directory. After requiring,
-
# # assumes MyApp constant contains Rack application
-
1
def self.parse_file(config, opts = Server::Options.new)
-
if config.end_with?('.ru')
-
return self.load_file(config, opts)
-
else
-
require config
-
app = Object.const_get(::File.basename(config, '.rb').split('_').map(&:capitalize).join(''))
-
return app, {}
-
end
-
end
-
-
# Load the given file as a rackup file, treating the
-
# contents as if specified inside a Rack::Builder block.
-
#
-
# Treats the first comment at the beginning of a line
-
# that starts with a backslash as options similar to
-
# options passed on a rackup command line.
-
#
-
# Ignores content in the file after +__END__+, so that
-
# use of +__END__+ will not result in a syntax error.
-
#
-
# Example config.ru file:
-
#
-
# $ cat config.ru
-
#
-
# #\ -p 9393
-
#
-
# use Rack::ContentLength
-
# require './app.rb'
-
# run App
-
1
def self.load_file(path, opts = Server::Options.new)
-
options = {}
-
-
cfgfile = ::File.read(path)
-
cfgfile.slice!(/\A#{UTF_8_BOM}/) if cfgfile.encoding == Encoding::UTF_8
-
-
if cfgfile[/^#\\(.*)/] && opts
-
warn "Parsing options from the first comment line is deprecated!"
-
options = opts.parse! $1.split(/\s+/)
-
end
-
-
cfgfile.sub!(/^__END__\n.*\Z/m, '')
-
app = new_from_string cfgfile, path
-
-
return app, options
-
end
-
-
# Evaluate the given +builder_script+ string in the context of
-
# a Rack::Builder block, returning a Rack application.
-
1
def self.new_from_string(builder_script, file = "(rackup)")
-
# We want to build a variant of TOPLEVEL_BINDING with self as a Rack::Builder instance.
-
# We cannot use instance_eval(String) as that would resolve constants differently.
-
binding, builder = TOPLEVEL_BINDING.eval('Rack::Builder.new.instance_eval { [binding, self] }')
-
eval builder_script, binding, file
-
builder.to_app
-
end
-
-
# Initialize a new Rack::Builder instance. +default_app+ specifies the
-
# default application if +run+ is not called later. If a block
-
# is given, it is evaluted in the context of the instance.
-
1
def initialize(default_app = nil, &block)
-
4
@use, @map, @run, @warmup, @freeze_app = [], nil, default_app, nil, false
-
4
instance_eval(&block) if block_given?
-
end
-
-
# Create a new Rack::Builder instance and return the Rack application
-
# generated from it.
-
1
def self.app(default_app = nil, &block)
-
self.new(default_app, &block).to_app
-
end
-
-
# Specifies middleware to use in a stack.
-
#
-
# class Middleware
-
# def initialize(app)
-
# @app = app
-
# end
-
#
-
# def call(env)
-
# env["rack.some_header"] = "setting an example"
-
# @app.call(env)
-
# end
-
# end
-
#
-
# use Middleware
-
# run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
-
#
-
# All requests through to this application will first be processed by the middleware class.
-
# The +call+ method in this example sets an additional environment key which then can be
-
# referenced in the application if required.
-
1
def use(middleware, *args, &block)
-
20
if @map
-
mapping, @map = @map, nil
-
@use << proc { |app| generate_map(app, mapping) }
-
end
-
40
@use << proc { |app| middleware.new(app, *args, &block) }
-
end
-
1
ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
-
-
# Takes an argument that is an object that responds to #call and returns a Rack response.
-
# The simplest form of this is a lambda object:
-
#
-
# run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
-
#
-
# However this could also be a class:
-
#
-
# class Heartbeat
-
# def self.call(env)
-
# [200, { "Content-Type" => "text/plain" }, ["OK"]]
-
# end
-
# end
-
#
-
# run Heartbeat
-
1
def run(app)
-
4
@run = app
-
end
-
-
# Takes a lambda or block that is used to warm-up the application. This block is called
-
# before the Rack application is returned by to_app.
-
#
-
# warmup do |app|
-
# client = Rack::MockRequest.new(app)
-
# client.get('/')
-
# end
-
#
-
# use SomeMiddleware
-
# run MyApp
-
1
def warmup(prc = nil, &block)
-
@warmup = prc || block
-
end
-
-
# Creates a route within the application. Routes under the mapped path will be sent to
-
# the Rack application specified by run inside the block. Other requests will be sent to the
-
# default application specified by run outside the block.
-
#
-
# Rack::Builder.app do
-
# map '/heartbeat' do
-
# run Heartbeat
-
# end
-
# run App
-
# end
-
#
-
# The +use+ method can also be used inside the block to specify middleware to run under a specific path:
-
#
-
# Rack::Builder.app do
-
# map '/heartbeat' do
-
# use Middleware
-
# run Heartbeat
-
# end
-
# run App
-
# end
-
#
-
# This example includes a piece of middleware which will run before +/heartbeat+ requests hit +Heartbeat+.
-
#
-
# Note that providing a +path+ of +/+ will ignore any default application given in a +run+ statement
-
# outside the block.
-
1
def map(path, &block)
-
@map ||= {}
-
@map[path] = block
-
end
-
-
# Freeze the app (set using run) and all middleware instances when building the application
-
# in to_app.
-
1
def freeze_app
-
@freeze_app = true
-
end
-
-
# Return the Rack application generated by this instance.
-
1
def to_app
-
4
app = @map ? generate_map(@run, @map) : @run
-
4
fail "missing run or map statement" unless app
-
4
app.freeze if @freeze_app
-
44
app = @use.reverse.inject(app) { |a, e| e[a].tap { |x| x.freeze if @freeze_app } }
-
4
@warmup.call(app) if @warmup
-
4
app
-
end
-
-
# Call the Rack application generated by this builder instance. Note that
-
# this rebuilds the Rack application and runs the warmup code (if any)
-
# every time it is called, so it should not be used if performance is important.
-
1
def call(env)
-
to_app.call(env)
-
end
-
-
1
private
-
-
# Generate a URLMap instance by generating new Rack applications for each
-
# map block in this instance.
-
1
def generate_map(default_app, mapping)
-
mapped = default_app ? { '/' => default_app } : {}
-
mapping.each { |r, b| mapped[r] = self.class.new(default_app, &b).to_app }
-
URLMap.new(mapped)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Rack
-
# Rack::CommonLogger forwards every request to the given +app+, and
-
# logs a line in the
-
# {Apache common log format}[http://httpd.apache.org/docs/1.3/logs.html#common]
-
# to the configured logger.
-
1
class CommonLogger
-
# Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common
-
#
-
# lilith.local - - [07/Aug/2006 23:58:02 -0400] "GET / HTTP/1.1" 500 -
-
#
-
# %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
-
#
-
# The actual format is slightly different than the above due to the
-
# separation of SCRIPT_NAME and PATH_INFO, and because the elapsed
-
# time in seconds is included at the end.
-
1
FORMAT = %{%s - %s [%s] "%s %s%s%s %s" %d %s %0.4f\n}
-
-
# +logger+ can be any object that supports the +write+ or +<<+ methods,
-
# which includes the standard library Logger. These methods are called
-
# with a single string argument, the log message.
-
# If +logger+ is nil, CommonLogger will fall back <tt>env['rack.errors']</tt>.
-
1
def initialize(app, logger = nil)
-
@app = app
-
@logger = logger
-
end
-
-
# Log all requests in common_log format after a response has been
-
# returned. Note that if the app raises an exception, the request
-
# will not be logged, so if exception handling middleware are used,
-
# they should be loaded after this middleware. Additionally, because
-
# the logging happens after the request body has been fully sent, any
-
# exceptions raised during the sending of the response body will
-
# cause the request not to be logged.
-
1
def call(env)
-
began_at = Utils.clock_time
-
status, headers, body = @app.call(env)
-
headers = Utils::HeaderHash[headers]
-
body = BodyProxy.new(body) { log(env, status, headers, began_at) }
-
[status, headers, body]
-
end
-
-
1
private
-
-
# Log the request to the configured logger.
-
1
def log(env, status, header, began_at)
-
length = extract_content_length(header)
-
-
msg = FORMAT % [
-
env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
-
env["REMOTE_USER"] || "-",
-
Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"),
-
env[REQUEST_METHOD],
-
env[SCRIPT_NAME],
-
env[PATH_INFO],
-
env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
-
env[SERVER_PROTOCOL],
-
status.to_s[0..3],
-
length,
-
Utils.clock_time - began_at ]
-
-
logger = @logger || env[RACK_ERRORS]
-
# Standard library logger doesn't support write but it supports << which actually
-
# calls to write on the log device without formatting
-
if logger.respond_to?(:write)
-
logger.write(msg)
-
else
-
logger << msg
-
end
-
end
-
-
# Attempt to determine the content length for the response to
-
# include it in the logged data.
-
1
def extract_content_length(headers)
-
value = headers[CONTENT_LENGTH]
-
!value || value.to_s == '0' ? '-' : value
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative 'files'
-
-
1
module Rack
-
1
File = Files
-
end
-
# frozen_string_literal: true
-
-
1
require 'time'
-
-
1
module Rack
-
# Rack::Files serves files below the +root+ directory given, according to the
-
# path info of the Rack request.
-
# e.g. when Rack::Files.new("/etc") is used, you can access 'passwd' file
-
# as http://localhost:9292/passwd
-
#
-
# Handlers can detect if bodies are a Rack::Files, and use mechanisms
-
# like sendfile on the +path+.
-
-
1
class Files
-
1
ALLOWED_VERBS = %w[GET HEAD OPTIONS]
-
1
ALLOW_HEADER = ALLOWED_VERBS.join(', ')
-
1
MULTIPART_BOUNDARY = 'AaB03x'
-
-
# @todo remove in 3.0
-
1
def self.method_added(name)
-
8
if name == :response_body
-
raise "#{self.class}\#response_body is no longer supported."
-
end
-
8
super
-
end
-
-
1
attr_reader :root
-
-
1
def initialize(root, headers = {}, default_mime = 'text/plain')
-
@root = (::File.expand_path(root) if root)
-
@headers = headers
-
@default_mime = default_mime
-
@head = Rack::Head.new(lambda { |env| get env })
-
end
-
-
1
def call(env)
-
# HEAD requests drop the response body, including 4xx error messages.
-
@head.call env
-
end
-
-
1
def get(env)
-
request = Rack::Request.new env
-
unless ALLOWED_VERBS.include? request.request_method
-
return fail(405, "Method Not Allowed", { 'Allow' => ALLOW_HEADER })
-
end
-
-
path_info = Utils.unescape_path request.path_info
-
return fail(400, "Bad Request") unless Utils.valid_path?(path_info)
-
-
clean_path_info = Utils.clean_path_info(path_info)
-
path = ::File.join(@root, clean_path_info)
-
-
available = begin
-
::File.file?(path) && ::File.readable?(path)
-
rescue SystemCallError
-
# Not sure in what conditions this exception can occur, but this
-
# is a safe way to handle such an error.
-
skipped
# :nocov:
-
skipped
false
-
skipped
# :nocov:
-
end
-
-
if available
-
serving(request, path)
-
else
-
fail(404, "File not found: #{path_info}")
-
end
-
end
-
-
1
def serving(request, path)
-
if request.options?
-
return [200, { 'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []]
-
end
-
last_modified = ::File.mtime(path).httpdate
-
return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified
-
-
headers = { "Last-Modified" => last_modified }
-
mime_type = mime_type path, @default_mime
-
headers[CONTENT_TYPE] = mime_type if mime_type
-
-
# Set custom headers
-
headers.merge!(@headers) if @headers
-
-
status = 200
-
size = filesize path
-
-
ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size)
-
if ranges.nil?
-
# No ranges:
-
ranges = [0..size - 1]
-
elsif ranges.empty?
-
# Unsatisfiable. Return error, and file size:
-
response = fail(416, "Byte range unsatisfiable")
-
response[1]["Content-Range"] = "bytes */#{size}"
-
return response
-
elsif ranges.size >= 1
-
# Partial content
-
partial_content = true
-
-
if ranges.size == 1
-
range = ranges[0]
-
headers["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}"
-
else
-
headers[CONTENT_TYPE] = "multipart/byteranges; boundary=#{MULTIPART_BOUNDARY}"
-
end
-
-
status = 206
-
body = BaseIterator.new(path, ranges, mime_type: mime_type, size: size)
-
size = body.bytesize
-
end
-
-
headers[CONTENT_LENGTH] = size.to_s
-
-
if request.head?
-
body = []
-
elsif !partial_content
-
body = Iterator.new(path, ranges, mime_type: mime_type, size: size)
-
end
-
-
[status, headers, body]
-
end
-
-
1
class BaseIterator
-
1
attr_reader :path, :ranges, :options
-
-
1
def initialize(path, ranges, options)
-
@path = path
-
@ranges = ranges
-
@options = options
-
end
-
-
1
def each
-
::File.open(path, "rb") do |file|
-
ranges.each do |range|
-
yield multipart_heading(range) if multipart?
-
-
each_range_part(file, range) do |part|
-
yield part
-
end
-
end
-
-
yield "\r\n--#{MULTIPART_BOUNDARY}--\r\n" if multipart?
-
end
-
end
-
-
1
def bytesize
-
size = ranges.inject(0) do |sum, range|
-
sum += multipart_heading(range).bytesize if multipart?
-
sum += range.size
-
end
-
size += "\r\n--#{MULTIPART_BOUNDARY}--\r\n".bytesize if multipart?
-
size
-
end
-
-
1
def close; end
-
-
1
private
-
-
1
def multipart?
-
ranges.size > 1
-
end
-
-
1
def multipart_heading(range)
-
<<-EOF
-
\r
-
--#{MULTIPART_BOUNDARY}\r
-
Content-Type: #{options[:mime_type]}\r
-
Content-Range: bytes #{range.begin}-#{range.end}/#{options[:size]}\r
-
\r
-
EOF
-
end
-
-
1
def each_range_part(file, range)
-
file.seek(range.begin)
-
remaining_len = range.end - range.begin + 1
-
while remaining_len > 0
-
part = file.read([8192, remaining_len].min)
-
break unless part
-
remaining_len -= part.length
-
-
yield part
-
end
-
end
-
end
-
-
1
class Iterator < BaseIterator
-
1
alias :to_path :path
-
end
-
-
1
private
-
-
1
def fail(status, body, headers = {})
-
body += "\n"
-
-
[
-
status,
-
{
-
CONTENT_TYPE => "text/plain",
-
CONTENT_LENGTH => body.size.to_s,
-
"X-Cascade" => "pass"
-
}.merge!(headers),
-
[body]
-
]
-
end
-
-
# The MIME type for the contents of the file located at @path
-
1
def mime_type(path, default_mime)
-
Mime.mime_type(::File.extname(path), default_mime)
-
end
-
-
1
def filesize(path)
-
# We check via File::size? whether this file provides size info
-
# via stat (e.g. /proc files often don't), otherwise we have to
-
# figure it out by reading the whole file into memory.
-
::File.size?(path) || ::File.read(path).bytesize
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Rack
-
# Rack::Head returns an empty body for all HEAD requests. It leaves
-
# all other requests unchanged.
-
1
class Head
-
1
def initialize(app)
-
2
@app = app
-
end
-
-
1
def call(env)
-
2
status, headers, body = @app.call(env)
-
-
2
if env[REQUEST_METHOD] == HEAD
-
[
-
status, headers, Rack::BodyProxy.new([]) do
-
body.close if body.respond_to? :close
-
end
-
]
-
else
-
2
[status, headers, body]
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Rack
-
# Rack::MediaType parse media type and parameters out of content_type string
-
-
1
class MediaType
-
1
SPLIT_PATTERN = %r{\s*[;,]\s*}
-
-
1
class << self
-
# The media type (type/subtype) portion of the CONTENT_TYPE header
-
# without any media type parameters. e.g., when CONTENT_TYPE is
-
# "text/plain;charset=utf-8", the media-type is "text/plain".
-
#
-
# For more information on the use of media types in HTTP, see:
-
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
-
1
def type(content_type)
-
4
return nil unless content_type
-
content_type.split(SPLIT_PATTERN, 2).first.tap &:downcase!
-
end
-
-
# The media type parameters provided in CONTENT_TYPE as a Hash, or
-
# an empty Hash if no CONTENT_TYPE or media-type parameters were
-
# provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
-
# this method responds with the following Hash:
-
# { 'charset' => 'utf-8' }
-
1
def params(content_type)
-
return {} if content_type.nil?
-
-
content_type.split(SPLIT_PATTERN)[1..-1].each_with_object({}) do |s, hsh|
-
k, v = s.split('=', 2)
-
-
hsh[k.tap(&:downcase!)] = strip_doublequotes(v)
-
end
-
end
-
-
1
private
-
-
1
def strip_doublequotes(str)
-
(str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Rack
-
1
module Mime
-
# Returns String with mime type if found, otherwise use +fallback+.
-
# +ext+ should be filename extension in the '.ext' format that
-
# File.extname(file) returns.
-
# +fallback+ may be any object
-
#
-
# Also see the documentation for MIME_TYPES
-
#
-
# Usage:
-
# Rack::Mime.mime_type('.foo')
-
#
-
# This is a shortcut for:
-
# Rack::Mime::MIME_TYPES.fetch('.foo', 'application/octet-stream')
-
-
1
def mime_type(ext, fallback = 'application/octet-stream')
-
16
MIME_TYPES.fetch(ext.to_s.downcase, fallback)
-
end
-
1
module_function :mime_type
-
-
# Returns true if the given value is a mime match for the given mime match
-
# specification, false otherwise.
-
#
-
# Rack::Mime.match?('text/html', 'text/*') => true
-
# Rack::Mime.match?('text/plain', '*') => true
-
# Rack::Mime.match?('text/html', 'application/json') => false
-
-
1
def match?(value, matcher)
-
v1, v2 = value.split('/', 2)
-
m1, m2 = matcher.split('/', 2)
-
-
(m1 == '*' || v1 == m1) && (m2.nil? || m2 == '*' || m2 == v2)
-
end
-
1
module_function :match?
-
-
# List of most common mime-types, selected various sources
-
# according to their usefulness in a webserving scope for Ruby
-
# users.
-
#
-
# To amend this list with your local mime.types list you can use:
-
#
-
# require 'webrick/httputils'
-
# list = WEBrick::HTTPUtils.load_mime_types('/etc/mime.types')
-
# Rack::Mime::MIME_TYPES.merge!(list)
-
#
-
# N.B. On Ubuntu the mime.types file does not include the leading period, so
-
# users may need to modify the data before merging into the hash.
-
-
1
MIME_TYPES = {
-
".123" => "application/vnd.lotus-1-2-3",
-
".3dml" => "text/vnd.in3d.3dml",
-
".3g2" => "video/3gpp2",
-
".3gp" => "video/3gpp",
-
".a" => "application/octet-stream",
-
".acc" => "application/vnd.americandynamics.acc",
-
".ace" => "application/x-ace-compressed",
-
".acu" => "application/vnd.acucobol",
-
".aep" => "application/vnd.audiograph",
-
".afp" => "application/vnd.ibm.modcap",
-
".ai" => "application/postscript",
-
".aif" => "audio/x-aiff",
-
".aiff" => "audio/x-aiff",
-
".ami" => "application/vnd.amiga.ami",
-
".appcache" => "text/cache-manifest",
-
".apr" => "application/vnd.lotus-approach",
-
".asc" => "application/pgp-signature",
-
".asf" => "video/x-ms-asf",
-
".asm" => "text/x-asm",
-
".aso" => "application/vnd.accpac.simply.aso",
-
".asx" => "video/x-ms-asf",
-
".atc" => "application/vnd.acucorp",
-
".atom" => "application/atom+xml",
-
".atomcat" => "application/atomcat+xml",
-
".atomsvc" => "application/atomsvc+xml",
-
".atx" => "application/vnd.antix.game-component",
-
".au" => "audio/basic",
-
".avi" => "video/x-msvideo",
-
".bat" => "application/x-msdownload",
-
".bcpio" => "application/x-bcpio",
-
".bdm" => "application/vnd.syncml.dm+wbxml",
-
".bh2" => "application/vnd.fujitsu.oasysprs",
-
".bin" => "application/octet-stream",
-
".bmi" => "application/vnd.bmi",
-
".bmp" => "image/bmp",
-
".box" => "application/vnd.previewsystems.box",
-
".btif" => "image/prs.btif",
-
".bz" => "application/x-bzip",
-
".bz2" => "application/x-bzip2",
-
".c" => "text/x-c",
-
".c4g" => "application/vnd.clonk.c4group",
-
".cab" => "application/vnd.ms-cab-compressed",
-
".cc" => "text/x-c",
-
".ccxml" => "application/ccxml+xml",
-
".cdbcmsg" => "application/vnd.contact.cmsg",
-
".cdkey" => "application/vnd.mediastation.cdkey",
-
".cdx" => "chemical/x-cdx",
-
".cdxml" => "application/vnd.chemdraw+xml",
-
".cdy" => "application/vnd.cinderella",
-
".cer" => "application/pkix-cert",
-
".cgm" => "image/cgm",
-
".chat" => "application/x-chat",
-
".chm" => "application/vnd.ms-htmlhelp",
-
".chrt" => "application/vnd.kde.kchart",
-
".cif" => "chemical/x-cif",
-
".cii" => "application/vnd.anser-web-certificate-issue-initiation",
-
".cil" => "application/vnd.ms-artgalry",
-
".cla" => "application/vnd.claymore",
-
".class" => "application/octet-stream",
-
".clkk" => "application/vnd.crick.clicker.keyboard",
-
".clkp" => "application/vnd.crick.clicker.palette",
-
".clkt" => "application/vnd.crick.clicker.template",
-
".clkw" => "application/vnd.crick.clicker.wordbank",
-
".clkx" => "application/vnd.crick.clicker",
-
".clp" => "application/x-msclip",
-
".cmc" => "application/vnd.cosmocaller",
-
".cmdf" => "chemical/x-cmdf",
-
".cml" => "chemical/x-cml",
-
".cmp" => "application/vnd.yellowriver-custom-menu",
-
".cmx" => "image/x-cmx",
-
".com" => "application/x-msdownload",
-
".conf" => "text/plain",
-
".cpio" => "application/x-cpio",
-
".cpp" => "text/x-c",
-
".cpt" => "application/mac-compactpro",
-
".crd" => "application/x-mscardfile",
-
".crl" => "application/pkix-crl",
-
".crt" => "application/x-x509-ca-cert",
-
".csh" => "application/x-csh",
-
".csml" => "chemical/x-csml",
-
".csp" => "application/vnd.commonspace",
-
".css" => "text/css",
-
".csv" => "text/csv",
-
".curl" => "application/vnd.curl",
-
".cww" => "application/prs.cww",
-
".cxx" => "text/x-c",
-
".daf" => "application/vnd.mobius.daf",
-
".davmount" => "application/davmount+xml",
-
".dcr" => "application/x-director",
-
".dd2" => "application/vnd.oma.dd2+xml",
-
".ddd" => "application/vnd.fujixerox.ddd",
-
".deb" => "application/x-debian-package",
-
".der" => "application/x-x509-ca-cert",
-
".dfac" => "application/vnd.dreamfactory",
-
".diff" => "text/x-diff",
-
".dis" => "application/vnd.mobius.dis",
-
".djv" => "image/vnd.djvu",
-
".djvu" => "image/vnd.djvu",
-
".dll" => "application/x-msdownload",
-
".dmg" => "application/octet-stream",
-
".dna" => "application/vnd.dna",
-
".doc" => "application/msword",
-
".docm" => "application/vnd.ms-word.document.macroEnabled.12",
-
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
-
".dot" => "application/msword",
-
".dotm" => "application/vnd.ms-word.template.macroEnabled.12",
-
".dotx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
-
".dp" => "application/vnd.osgi.dp",
-
".dpg" => "application/vnd.dpgraph",
-
".dsc" => "text/prs.lines.tag",
-
".dtd" => "application/xml-dtd",
-
".dts" => "audio/vnd.dts",
-
".dtshd" => "audio/vnd.dts.hd",
-
".dv" => "video/x-dv",
-
".dvi" => "application/x-dvi",
-
".dwf" => "model/vnd.dwf",
-
".dwg" => "image/vnd.dwg",
-
".dxf" => "image/vnd.dxf",
-
".dxp" => "application/vnd.spotfire.dxp",
-
".ear" => "application/java-archive",
-
".ecelp4800" => "audio/vnd.nuera.ecelp4800",
-
".ecelp7470" => "audio/vnd.nuera.ecelp7470",
-
".ecelp9600" => "audio/vnd.nuera.ecelp9600",
-
".ecma" => "application/ecmascript",
-
".edm" => "application/vnd.novadigm.edm",
-
".edx" => "application/vnd.novadigm.edx",
-
".efif" => "application/vnd.picsel",
-
".ei6" => "application/vnd.pg.osasli",
-
".eml" => "message/rfc822",
-
".eol" => "audio/vnd.digital-winds",
-
".eot" => "application/vnd.ms-fontobject",
-
".eps" => "application/postscript",
-
".es3" => "application/vnd.eszigno3+xml",
-
".esf" => "application/vnd.epson.esf",
-
".etx" => "text/x-setext",
-
".exe" => "application/x-msdownload",
-
".ext" => "application/vnd.novadigm.ext",
-
".ez" => "application/andrew-inset",
-
".ez2" => "application/vnd.ezpix-album",
-
".ez3" => "application/vnd.ezpix-package",
-
".f" => "text/x-fortran",
-
".f77" => "text/x-fortran",
-
".f90" => "text/x-fortran",
-
".fbs" => "image/vnd.fastbidsheet",
-
".fdf" => "application/vnd.fdf",
-
".fe_launch" => "application/vnd.denovo.fcselayout-link",
-
".fg5" => "application/vnd.fujitsu.oasysgp",
-
".fli" => "video/x-fli",
-
".flo" => "application/vnd.micrografx.flo",
-
".flv" => "video/x-flv",
-
".flw" => "application/vnd.kde.kivio",
-
".flx" => "text/vnd.fmi.flexstor",
-
".fly" => "text/vnd.fly",
-
".fm" => "application/vnd.framemaker",
-
".fnc" => "application/vnd.frogans.fnc",
-
".for" => "text/x-fortran",
-
".fpx" => "image/vnd.fpx",
-
".fsc" => "application/vnd.fsc.weblaunch",
-
".fst" => "image/vnd.fst",
-
".ftc" => "application/vnd.fluxtime.clip",
-
".fti" => "application/vnd.anser-web-funds-transfer-initiation",
-
".fvt" => "video/vnd.fvt",
-
".fzs" => "application/vnd.fuzzysheet",
-
".g3" => "image/g3fax",
-
".gac" => "application/vnd.groove-account",
-
".gdl" => "model/vnd.gdl",
-
".gem" => "application/octet-stream",
-
".gemspec" => "text/x-script.ruby",
-
".ghf" => "application/vnd.groove-help",
-
".gif" => "image/gif",
-
".gim" => "application/vnd.groove-identity-message",
-
".gmx" => "application/vnd.gmx",
-
".gph" => "application/vnd.flographit",
-
".gqf" => "application/vnd.grafeq",
-
".gram" => "application/srgs",
-
".grv" => "application/vnd.groove-injector",
-
".grxml" => "application/srgs+xml",
-
".gtar" => "application/x-gtar",
-
".gtm" => "application/vnd.groove-tool-message",
-
".gtw" => "model/vnd.gtw",
-
".gv" => "text/vnd.graphviz",
-
".gz" => "application/x-gzip",
-
".h" => "text/x-c",
-
".h261" => "video/h261",
-
".h263" => "video/h263",
-
".h264" => "video/h264",
-
".hbci" => "application/vnd.hbci",
-
".hdf" => "application/x-hdf",
-
".hh" => "text/x-c",
-
".hlp" => "application/winhlp",
-
".hpgl" => "application/vnd.hp-hpgl",
-
".hpid" => "application/vnd.hp-hpid",
-
".hps" => "application/vnd.hp-hps",
-
".hqx" => "application/mac-binhex40",
-
".htc" => "text/x-component",
-
".htke" => "application/vnd.kenameaapp",
-
".htm" => "text/html",
-
".html" => "text/html",
-
".hvd" => "application/vnd.yamaha.hv-dic",
-
".hvp" => "application/vnd.yamaha.hv-voice",
-
".hvs" => "application/vnd.yamaha.hv-script",
-
".icc" => "application/vnd.iccprofile",
-
".ice" => "x-conference/x-cooltalk",
-
".ico" => "image/vnd.microsoft.icon",
-
".ics" => "text/calendar",
-
".ief" => "image/ief",
-
".ifb" => "text/calendar",
-
".ifm" => "application/vnd.shana.informed.formdata",
-
".igl" => "application/vnd.igloader",
-
".igs" => "model/iges",
-
".igx" => "application/vnd.micrografx.igx",
-
".iif" => "application/vnd.shana.informed.interchange",
-
".imp" => "application/vnd.accpac.simply.imp",
-
".ims" => "application/vnd.ms-ims",
-
".ipk" => "application/vnd.shana.informed.package",
-
".irm" => "application/vnd.ibm.rights-management",
-
".irp" => "application/vnd.irepository.package+xml",
-
".iso" => "application/octet-stream",
-
".itp" => "application/vnd.shana.informed.formtemplate",
-
".ivp" => "application/vnd.immervision-ivp",
-
".ivu" => "application/vnd.immervision-ivu",
-
".jad" => "text/vnd.sun.j2me.app-descriptor",
-
".jam" => "application/vnd.jam",
-
".jar" => "application/java-archive",
-
".java" => "text/x-java-source",
-
".jisp" => "application/vnd.jisp",
-
".jlt" => "application/vnd.hp-jlyt",
-
".jnlp" => "application/x-java-jnlp-file",
-
".joda" => "application/vnd.joost.joda-archive",
-
".jp2" => "image/jp2",
-
".jpeg" => "image/jpeg",
-
".jpg" => "image/jpeg",
-
".jpgv" => "video/jpeg",
-
".jpm" => "video/jpm",
-
".js" => "application/javascript",
-
".json" => "application/json",
-
".karbon" => "application/vnd.kde.karbon",
-
".kfo" => "application/vnd.kde.kformula",
-
".kia" => "application/vnd.kidspiration",
-
".kml" => "application/vnd.google-earth.kml+xml",
-
".kmz" => "application/vnd.google-earth.kmz",
-
".kne" => "application/vnd.kinar",
-
".kon" => "application/vnd.kde.kontour",
-
".kpr" => "application/vnd.kde.kpresenter",
-
".ksp" => "application/vnd.kde.kspread",
-
".ktz" => "application/vnd.kahootz",
-
".kwd" => "application/vnd.kde.kword",
-
".latex" => "application/x-latex",
-
".lbd" => "application/vnd.llamagraphics.life-balance.desktop",
-
".lbe" => "application/vnd.llamagraphics.life-balance.exchange+xml",
-
".les" => "application/vnd.hhe.lesson-player",
-
".link66" => "application/vnd.route66.link66+xml",
-
".log" => "text/plain",
-
".lostxml" => "application/lost+xml",
-
".lrm" => "application/vnd.ms-lrm",
-
".ltf" => "application/vnd.frogans.ltf",
-
".lvp" => "audio/vnd.lucent.voice",
-
".lwp" => "application/vnd.lotus-wordpro",
-
".m3u" => "audio/x-mpegurl",
-
".m3u8" => "application/x-mpegurl",
-
".m4a" => "audio/mp4a-latm",
-
".m4v" => "video/mp4",
-
".ma" => "application/mathematica",
-
".mag" => "application/vnd.ecowin.chart",
-
".man" => "text/troff",
-
".manifest" => "text/cache-manifest",
-
".mathml" => "application/mathml+xml",
-
".mbk" => "application/vnd.mobius.mbk",
-
".mbox" => "application/mbox",
-
".mc1" => "application/vnd.medcalcdata",
-
".mcd" => "application/vnd.mcd",
-
".mdb" => "application/x-msaccess",
-
".mdi" => "image/vnd.ms-modi",
-
".mdoc" => "text/troff",
-
".me" => "text/troff",
-
".mfm" => "application/vnd.mfmp",
-
".mgz" => "application/vnd.proteus.magazine",
-
".mid" => "audio/midi",
-
".midi" => "audio/midi",
-
".mif" => "application/vnd.mif",
-
".mime" => "message/rfc822",
-
".mj2" => "video/mj2",
-
".mlp" => "application/vnd.dolby.mlp",
-
".mmd" => "application/vnd.chipnuts.karaoke-mmd",
-
".mmf" => "application/vnd.smaf",
-
".mml" => "application/mathml+xml",
-
".mmr" => "image/vnd.fujixerox.edmics-mmr",
-
".mng" => "video/x-mng",
-
".mny" => "application/x-msmoney",
-
".mov" => "video/quicktime",
-
".movie" => "video/x-sgi-movie",
-
".mp3" => "audio/mpeg",
-
".mp4" => "video/mp4",
-
".mp4a" => "audio/mp4",
-
".mp4s" => "application/mp4",
-
".mp4v" => "video/mp4",
-
".mpc" => "application/vnd.mophun.certificate",
-
".mpd" => "application/dash+xml",
-
".mpeg" => "video/mpeg",
-
".mpg" => "video/mpeg",
-
".mpga" => "audio/mpeg",
-
".mpkg" => "application/vnd.apple.installer+xml",
-
".mpm" => "application/vnd.blueice.multipass",
-
".mpn" => "application/vnd.mophun.application",
-
".mpp" => "application/vnd.ms-project",
-
".mpy" => "application/vnd.ibm.minipay",
-
".mqy" => "application/vnd.mobius.mqy",
-
".mrc" => "application/marc",
-
".ms" => "text/troff",
-
".mscml" => "application/mediaservercontrol+xml",
-
".mseq" => "application/vnd.mseq",
-
".msf" => "application/vnd.epson.msf",
-
".msh" => "model/mesh",
-
".msi" => "application/x-msdownload",
-
".msl" => "application/vnd.mobius.msl",
-
".msty" => "application/vnd.muvee.style",
-
".mts" => "model/vnd.mts",
-
".mus" => "application/vnd.musician",
-
".mvb" => "application/x-msmediaview",
-
".mwf" => "application/vnd.mfer",
-
".mxf" => "application/mxf",
-
".mxl" => "application/vnd.recordare.musicxml",
-
".mxml" => "application/xv+xml",
-
".mxs" => "application/vnd.triscape.mxs",
-
".mxu" => "video/vnd.mpegurl",
-
".n" => "application/vnd.nokia.n-gage.symbian.install",
-
".nc" => "application/x-netcdf",
-
".ngdat" => "application/vnd.nokia.n-gage.data",
-
".nlu" => "application/vnd.neurolanguage.nlu",
-
".nml" => "application/vnd.enliven",
-
".nnd" => "application/vnd.noblenet-directory",
-
".nns" => "application/vnd.noblenet-sealer",
-
".nnw" => "application/vnd.noblenet-web",
-
".npx" => "image/vnd.net-fpx",
-
".nsf" => "application/vnd.lotus-notes",
-
".oa2" => "application/vnd.fujitsu.oasys2",
-
".oa3" => "application/vnd.fujitsu.oasys3",
-
".oas" => "application/vnd.fujitsu.oasys",
-
".obd" => "application/x-msbinder",
-
".oda" => "application/oda",
-
".odc" => "application/vnd.oasis.opendocument.chart",
-
".odf" => "application/vnd.oasis.opendocument.formula",
-
".odg" => "application/vnd.oasis.opendocument.graphics",
-
".odi" => "application/vnd.oasis.opendocument.image",
-
".odp" => "application/vnd.oasis.opendocument.presentation",
-
".ods" => "application/vnd.oasis.opendocument.spreadsheet",
-
".odt" => "application/vnd.oasis.opendocument.text",
-
".oga" => "audio/ogg",
-
".ogg" => "application/ogg",
-
".ogv" => "video/ogg",
-
".ogx" => "application/ogg",
-
".org" => "application/vnd.lotus-organizer",
-
".otc" => "application/vnd.oasis.opendocument.chart-template",
-
".otf" => "application/vnd.oasis.opendocument.formula-template",
-
".otg" => "application/vnd.oasis.opendocument.graphics-template",
-
".oth" => "application/vnd.oasis.opendocument.text-web",
-
".oti" => "application/vnd.oasis.opendocument.image-template",
-
".otm" => "application/vnd.oasis.opendocument.text-master",
-
".ots" => "application/vnd.oasis.opendocument.spreadsheet-template",
-
".ott" => "application/vnd.oasis.opendocument.text-template",
-
".oxt" => "application/vnd.openofficeorg.extension",
-
".p" => "text/x-pascal",
-
".p10" => "application/pkcs10",
-
".p12" => "application/x-pkcs12",
-
".p7b" => "application/x-pkcs7-certificates",
-
".p7m" => "application/pkcs7-mime",
-
".p7r" => "application/x-pkcs7-certreqresp",
-
".p7s" => "application/pkcs7-signature",
-
".pas" => "text/x-pascal",
-
".pbd" => "application/vnd.powerbuilder6",
-
".pbm" => "image/x-portable-bitmap",
-
".pcl" => "application/vnd.hp-pcl",
-
".pclxl" => "application/vnd.hp-pclxl",
-
".pcx" => "image/x-pcx",
-
".pdb" => "chemical/x-pdb",
-
".pdf" => "application/pdf",
-
".pem" => "application/x-x509-ca-cert",
-
".pfr" => "application/font-tdpfr",
-
".pgm" => "image/x-portable-graymap",
-
".pgn" => "application/x-chess-pgn",
-
".pgp" => "application/pgp-encrypted",
-
".pic" => "image/x-pict",
-
".pict" => "image/pict",
-
".pkg" => "application/octet-stream",
-
".pki" => "application/pkixcmp",
-
".pkipath" => "application/pkix-pkipath",
-
".pl" => "text/x-script.perl",
-
".plb" => "application/vnd.3gpp.pic-bw-large",
-
".plc" => "application/vnd.mobius.plc",
-
".plf" => "application/vnd.pocketlearn",
-
".pls" => "application/pls+xml",
-
".pm" => "text/x-script.perl-module",
-
".pml" => "application/vnd.ctc-posml",
-
".png" => "image/png",
-
".pnm" => "image/x-portable-anymap",
-
".pntg" => "image/x-macpaint",
-
".portpkg" => "application/vnd.macports.portpkg",
-
".pot" => "application/vnd.ms-powerpoint",
-
".potm" => "application/vnd.ms-powerpoint.template.macroEnabled.12",
-
".potx" => "application/vnd.openxmlformats-officedocument.presentationml.template",
-
".ppa" => "application/vnd.ms-powerpoint",
-
".ppam" => "application/vnd.ms-powerpoint.addin.macroEnabled.12",
-
".ppd" => "application/vnd.cups-ppd",
-
".ppm" => "image/x-portable-pixmap",
-
".pps" => "application/vnd.ms-powerpoint",
-
".ppsm" => "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
-
".ppsx" => "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
-
".ppt" => "application/vnd.ms-powerpoint",
-
".pptm" => "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
-
".pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
-
".prc" => "application/vnd.palm",
-
".pre" => "application/vnd.lotus-freelance",
-
".prf" => "application/pics-rules",
-
".ps" => "application/postscript",
-
".psb" => "application/vnd.3gpp.pic-bw-small",
-
".psd" => "image/vnd.adobe.photoshop",
-
".ptid" => "application/vnd.pvi.ptid1",
-
".pub" => "application/x-mspublisher",
-
".pvb" => "application/vnd.3gpp.pic-bw-var",
-
".pwn" => "application/vnd.3m.post-it-notes",
-
".py" => "text/x-script.python",
-
".pya" => "audio/vnd.ms-playready.media.pya",
-
".pyv" => "video/vnd.ms-playready.media.pyv",
-
".qam" => "application/vnd.epson.quickanime",
-
".qbo" => "application/vnd.intu.qbo",
-
".qfx" => "application/vnd.intu.qfx",
-
".qps" => "application/vnd.publishare-delta-tree",
-
".qt" => "video/quicktime",
-
".qtif" => "image/x-quicktime",
-
".qxd" => "application/vnd.quark.quarkxpress",
-
".ra" => "audio/x-pn-realaudio",
-
".rake" => "text/x-script.ruby",
-
".ram" => "audio/x-pn-realaudio",
-
".rar" => "application/x-rar-compressed",
-
".ras" => "image/x-cmu-raster",
-
".rb" => "text/x-script.ruby",
-
".rcprofile" => "application/vnd.ipunplugged.rcprofile",
-
".rdf" => "application/rdf+xml",
-
".rdz" => "application/vnd.data-vision.rdz",
-
".rep" => "application/vnd.businessobjects",
-
".rgb" => "image/x-rgb",
-
".rif" => "application/reginfo+xml",
-
".rl" => "application/resource-lists+xml",
-
".rlc" => "image/vnd.fujixerox.edmics-rlc",
-
".rld" => "application/resource-lists-diff+xml",
-
".rm" => "application/vnd.rn-realmedia",
-
".rmp" => "audio/x-pn-realaudio-plugin",
-
".rms" => "application/vnd.jcp.javame.midlet-rms",
-
".rnc" => "application/relax-ng-compact-syntax",
-
".roff" => "text/troff",
-
".rpm" => "application/x-redhat-package-manager",
-
".rpss" => "application/vnd.nokia.radio-presets",
-
".rpst" => "application/vnd.nokia.radio-preset",
-
".rq" => "application/sparql-query",
-
".rs" => "application/rls-services+xml",
-
".rsd" => "application/rsd+xml",
-
".rss" => "application/rss+xml",
-
".rtf" => "application/rtf",
-
".rtx" => "text/richtext",
-
".ru" => "text/x-script.ruby",
-
".s" => "text/x-asm",
-
".saf" => "application/vnd.yamaha.smaf-audio",
-
".sbml" => "application/sbml+xml",
-
".sc" => "application/vnd.ibm.secure-container",
-
".scd" => "application/x-msschedule",
-
".scm" => "application/vnd.lotus-screencam",
-
".scq" => "application/scvp-cv-request",
-
".scs" => "application/scvp-cv-response",
-
".sdkm" => "application/vnd.solent.sdkm+xml",
-
".sdp" => "application/sdp",
-
".see" => "application/vnd.seemail",
-
".sema" => "application/vnd.sema",
-
".semd" => "application/vnd.semd",
-
".semf" => "application/vnd.semf",
-
".setpay" => "application/set-payment-initiation",
-
".setreg" => "application/set-registration-initiation",
-
".sfd" => "application/vnd.hydrostatix.sof-data",
-
".sfs" => "application/vnd.spotfire.sfs",
-
".sgm" => "text/sgml",
-
".sgml" => "text/sgml",
-
".sh" => "application/x-sh",
-
".shar" => "application/x-shar",
-
".shf" => "application/shf+xml",
-
".sig" => "application/pgp-signature",
-
".sit" => "application/x-stuffit",
-
".sitx" => "application/x-stuffitx",
-
".skp" => "application/vnd.koan",
-
".slt" => "application/vnd.epson.salt",
-
".smi" => "application/smil+xml",
-
".snd" => "audio/basic",
-
".so" => "application/octet-stream",
-
".spf" => "application/vnd.yamaha.smaf-phrase",
-
".spl" => "application/x-futuresplash",
-
".spot" => "text/vnd.in3d.spot",
-
".spp" => "application/scvp-vp-response",
-
".spq" => "application/scvp-vp-request",
-
".src" => "application/x-wais-source",
-
".srt" => "text/srt",
-
".srx" => "application/sparql-results+xml",
-
".sse" => "application/vnd.kodak-descriptor",
-
".ssf" => "application/vnd.epson.ssf",
-
".ssml" => "application/ssml+xml",
-
".stf" => "application/vnd.wt.stf",
-
".stk" => "application/hyperstudio",
-
".str" => "application/vnd.pg.format",
-
".sus" => "application/vnd.sus-calendar",
-
".sv4cpio" => "application/x-sv4cpio",
-
".sv4crc" => "application/x-sv4crc",
-
".svd" => "application/vnd.svd",
-
".svg" => "image/svg+xml",
-
".svgz" => "image/svg+xml",
-
".swf" => "application/x-shockwave-flash",
-
".swi" => "application/vnd.arastra.swi",
-
".t" => "text/troff",
-
".tao" => "application/vnd.tao.intent-module-archive",
-
".tar" => "application/x-tar",
-
".tbz" => "application/x-bzip-compressed-tar",
-
".tcap" => "application/vnd.3gpp2.tcap",
-
".tcl" => "application/x-tcl",
-
".tex" => "application/x-tex",
-
".texi" => "application/x-texinfo",
-
".texinfo" => "application/x-texinfo",
-
".text" => "text/plain",
-
".tif" => "image/tiff",
-
".tiff" => "image/tiff",
-
".tmo" => "application/vnd.tmobile-livetv",
-
".torrent" => "application/x-bittorrent",
-
".tpl" => "application/vnd.groove-tool-template",
-
".tpt" => "application/vnd.trid.tpt",
-
".tr" => "text/troff",
-
".tra" => "application/vnd.trueapp",
-
".trm" => "application/x-msterminal",
-
".ts" => "video/mp2t",
-
".tsv" => "text/tab-separated-values",
-
".ttf" => "application/octet-stream",
-
".twd" => "application/vnd.simtech-mindmapper",
-
".txd" => "application/vnd.genomatix.tuxedo",
-
".txf" => "application/vnd.mobius.txf",
-
".txt" => "text/plain",
-
".ufd" => "application/vnd.ufdl",
-
".umj" => "application/vnd.umajin",
-
".unityweb" => "application/vnd.unity",
-
".uoml" => "application/vnd.uoml+xml",
-
".uri" => "text/uri-list",
-
".ustar" => "application/x-ustar",
-
".utz" => "application/vnd.uiq.theme",
-
".uu" => "text/x-uuencode",
-
".vcd" => "application/x-cdlink",
-
".vcf" => "text/x-vcard",
-
".vcg" => "application/vnd.groove-vcard",
-
".vcs" => "text/x-vcalendar",
-
".vcx" => "application/vnd.vcx",
-
".vis" => "application/vnd.visionary",
-
".viv" => "video/vnd.vivo",
-
".vrml" => "model/vrml",
-
".vsd" => "application/vnd.visio",
-
".vsf" => "application/vnd.vsf",
-
".vtt" => "text/vtt",
-
".vtu" => "model/vnd.vtu",
-
".vxml" => "application/voicexml+xml",
-
".war" => "application/java-archive",
-
".wasm" => "application/wasm",
-
".wav" => "audio/x-wav",
-
".wax" => "audio/x-ms-wax",
-
".wbmp" => "image/vnd.wap.wbmp",
-
".wbs" => "application/vnd.criticaltools.wbs+xml",
-
".wbxml" => "application/vnd.wap.wbxml",
-
".webm" => "video/webm",
-
".wm" => "video/x-ms-wm",
-
".wma" => "audio/x-ms-wma",
-
".wmd" => "application/x-ms-wmd",
-
".wmf" => "application/x-msmetafile",
-
".wml" => "text/vnd.wap.wml",
-
".wmlc" => "application/vnd.wap.wmlc",
-
".wmls" => "text/vnd.wap.wmlscript",
-
".wmlsc" => "application/vnd.wap.wmlscriptc",
-
".wmv" => "video/x-ms-wmv",
-
".wmx" => "video/x-ms-wmx",
-
".wmz" => "application/x-ms-wmz",
-
".woff" => "application/font-woff",
-
".woff2" => "application/font-woff2",
-
".wpd" => "application/vnd.wordperfect",
-
".wpl" => "application/vnd.ms-wpl",
-
".wps" => "application/vnd.ms-works",
-
".wqd" => "application/vnd.wqd",
-
".wri" => "application/x-mswrite",
-
".wrl" => "model/vrml",
-
".wsdl" => "application/wsdl+xml",
-
".wspolicy" => "application/wspolicy+xml",
-
".wtb" => "application/vnd.webturbo",
-
".wvx" => "video/x-ms-wvx",
-
".x3d" => "application/vnd.hzn-3d-crossword",
-
".xar" => "application/vnd.xara",
-
".xbd" => "application/vnd.fujixerox.docuworks.binder",
-
".xbm" => "image/x-xbitmap",
-
".xdm" => "application/vnd.syncml.dm+xml",
-
".xdp" => "application/vnd.adobe.xdp+xml",
-
".xdw" => "application/vnd.fujixerox.docuworks",
-
".xenc" => "application/xenc+xml",
-
".xer" => "application/patch-ops-error+xml",
-
".xfdf" => "application/vnd.adobe.xfdf",
-
".xfdl" => "application/vnd.xfdl",
-
".xhtml" => "application/xhtml+xml",
-
".xif" => "image/vnd.xiff",
-
".xla" => "application/vnd.ms-excel",
-
".xlam" => "application/vnd.ms-excel.addin.macroEnabled.12",
-
".xls" => "application/vnd.ms-excel",
-
".xlsb" => "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
-
".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
-
".xlsm" => "application/vnd.ms-excel.sheet.macroEnabled.12",
-
".xlt" => "application/vnd.ms-excel",
-
".xltx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
-
".xml" => "application/xml",
-
".xo" => "application/vnd.olpc-sugar",
-
".xop" => "application/xop+xml",
-
".xpm" => "image/x-xpixmap",
-
".xpr" => "application/vnd.is-xpr",
-
".xps" => "application/vnd.ms-xpsdocument",
-
".xpw" => "application/vnd.intercon.formnet",
-
".xsl" => "application/xml",
-
".xslt" => "application/xslt+xml",
-
".xsm" => "application/vnd.syncml+xml",
-
".xspf" => "application/xspf+xml",
-
".xul" => "application/vnd.mozilla.xul+xml",
-
".xwd" => "image/x-xwindowdump",
-
".xyz" => "chemical/x-xyz",
-
".yaml" => "text/yaml",
-
".yml" => "text/yaml",
-
".zaz" => "application/vnd.zzazz.deck+xml",
-
".zip" => "application/zip",
-
".zmm" => "application/vnd.handheld-entertainment+xml",
-
}
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require 'uri'
-
1
require 'stringio'
-
1
require_relative '../rack'
-
1
require 'cgi/cookie'
-
-
1
module Rack
-
# Rack::MockRequest helps testing your Rack application without
-
# actually using HTTP.
-
#
-
# After performing a request on a URL with get/post/put/patch/delete, it
-
# returns a MockResponse with useful helper methods for effective
-
# testing.
-
#
-
# You can pass a hash with additional configuration to the
-
# get/post/put/patch/delete.
-
# <tt>:input</tt>:: A String or IO-like to be used as rack.input.
-
# <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
-
# <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
-
-
1
class MockRequest
-
1
class FatalWarning < RuntimeError
-
end
-
-
1
class FatalWarner
-
1
def puts(warning)
-
raise FatalWarning, warning
-
end
-
-
1
def write(warning)
-
raise FatalWarning, warning
-
end
-
-
1
def flush
-
end
-
-
1
def string
-
""
-
end
-
end
-
-
DEFAULT_ENV = {
-
1
RACK_VERSION => Rack::VERSION,
-
RACK_INPUT => StringIO.new,
-
RACK_ERRORS => StringIO.new,
-
RACK_MULTITHREAD => true,
-
RACK_MULTIPROCESS => true,
-
RACK_RUNONCE => false,
-
}.freeze
-
-
1
def initialize(app)
-
@app = app
-
end
-
-
# Make a GET request and return a MockResponse. See #request.
-
1
def get(uri, opts = {}) request(GET, uri, opts) end
-
# Make a POST request and return a MockResponse. See #request.
-
1
def post(uri, opts = {}) request(POST, uri, opts) end
-
# Make a PUT request and return a MockResponse. See #request.
-
1
def put(uri, opts = {}) request(PUT, uri, opts) end
-
# Make a PATCH request and return a MockResponse. See #request.
-
1
def patch(uri, opts = {}) request(PATCH, uri, opts) end
-
# Make a DELETE request and return a MockResponse. See #request.
-
1
def delete(uri, opts = {}) request(DELETE, uri, opts) end
-
# Make a HEAD request and return a MockResponse. See #request.
-
1
def head(uri, opts = {}) request(HEAD, uri, opts) end
-
# Make an OPTIONS request and return a MockResponse. See #request.
-
1
def options(uri, opts = {}) request(OPTIONS, uri, opts) end
-
-
# Make a request using the given request method for the given
-
# uri to the rack application and return a MockResponse.
-
# Options given are passed to MockRequest.env_for.
-
1
def request(method = GET, uri = "", opts = {})
-
env = self.class.env_for(uri, opts.merge(method: method))
-
-
if opts[:lint]
-
app = Rack::Lint.new(@app)
-
else
-
app = @app
-
end
-
-
errors = env[RACK_ERRORS]
-
status, headers, body = app.call(env)
-
MockResponse.new(status, headers, body, errors)
-
ensure
-
body.close if body.respond_to?(:close)
-
end
-
-
# For historical reasons, we're pinning to RFC 2396.
-
# URI::Parser = URI::RFC2396_Parser
-
1
def self.parse_uri_rfc2396(uri)
-
2
@parser ||= URI::Parser.new
-
2
@parser.parse(uri)
-
end
-
-
# Return the Rack environment used for a request to +uri+.
-
# All options that are strings are added to the returned environment.
-
# Options:
-
# :fatal :: Whether to raise an exception if request outputs to rack.errors
-
# :input :: The rack.input to set
-
# :method :: The HTTP request method to use
-
# :params :: The params to use
-
# :script_name :: The SCRIPT_NAME to set
-
1
def self.env_for(uri = "", opts = {})
-
2
uri = parse_uri_rfc2396(uri)
-
2
uri.path = "/#{uri.path}" unless uri.path[0] == ?/
-
-
2
env = DEFAULT_ENV.dup
-
-
2
env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : GET).b
-
2
env[SERVER_NAME] = (uri.host || "example.org").b
-
2
env[SERVER_PORT] = (uri.port ? uri.port.to_s : "80").b
-
2
env[QUERY_STRING] = (uri.query.to_s).b
-
2
env[PATH_INFO] = ((!uri.path || uri.path.empty?) ? "/" : uri.path).b
-
2
env[RACK_URL_SCHEME] = (uri.scheme || "http").b
-
2
env[HTTPS] = (env[RACK_URL_SCHEME] == "https" ? "on" : "off").b
-
-
2
env[SCRIPT_NAME] = opts[:script_name] || ""
-
-
2
if opts[:fatal]
-
env[RACK_ERRORS] = FatalWarner.new
-
else
-
2
env[RACK_ERRORS] = StringIO.new
-
end
-
-
2
if params = opts[:params]
-
if env[REQUEST_METHOD] == GET
-
params = Utils.parse_nested_query(params) if params.is_a?(String)
-
params.update(Utils.parse_nested_query(env[QUERY_STRING]))
-
env[QUERY_STRING] = Utils.build_nested_query(params)
-
elsif !opts.has_key?(:input)
-
opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
-
if params.is_a?(Hash)
-
if data = Rack::Multipart.build_multipart(params)
-
opts[:input] = data
-
opts["CONTENT_LENGTH"] ||= data.length.to_s
-
opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}"
-
else
-
opts[:input] = Utils.build_nested_query(params)
-
end
-
else
-
opts[:input] = params
-
end
-
end
-
end
-
-
2
empty_str = String.new
-
2
opts[:input] ||= empty_str
-
2
if String === opts[:input]
-
2
rack_input = StringIO.new(opts[:input])
-
else
-
rack_input = opts[:input]
-
end
-
-
2
rack_input.set_encoding(Encoding::BINARY)
-
2
env[RACK_INPUT] = rack_input
-
-
2
env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
-
-
2
opts.each { |field, value|
-
12
env[field] = value if String === field
-
}
-
-
2
env
-
end
-
end
-
-
# Rack::MockResponse provides useful helpers for testing your apps.
-
# Usually, you don't create the MockResponse on your own, but use
-
# MockRequest.
-
-
1
class MockResponse < Rack::Response
-
1
class << self
-
1
alias [] new
-
end
-
-
# Headers
-
1
attr_reader :original_headers, :cookies
-
-
# Errors
-
1
attr_accessor :errors
-
-
1
def initialize(status, headers, body, errors = StringIO.new(""))
-
2
@original_headers = headers
-
2
@errors = errors.string if errors.respond_to?(:string)
-
2
@cookies = parse_cookies_from_header
-
-
2
super(body, status, headers)
-
-
2
buffered_body!
-
end
-
-
1
def =~(other)
-
body =~ other
-
end
-
-
1
def match(other)
-
body.match other
-
end
-
-
1
def body
-
# FIXME: apparently users of MockResponse expect the return value of
-
# MockResponse#body to be a string. However, the real response object
-
# returns the body as a list.
-
#
-
# See spec_showstatus.rb:
-
#
-
# should "not replace existing messages" do
-
# ...
-
# res.body.should == "foo!"
-
# end
-
buffer = String.new
-
-
super.each do |chunk|
-
buffer << chunk
-
end
-
-
return buffer
-
end
-
-
1
def empty?
-
[201, 204, 304].include? status
-
end
-
-
1
def cookie(name)
-
cookies.fetch(name, nil)
-
end
-
-
1
private
-
-
1
def parse_cookies_from_header
-
2
cookies = Hash.new
-
2
if original_headers.has_key? 'Set-Cookie'
-
set_cookie_header = original_headers.fetch('Set-Cookie')
-
set_cookie_header.split("\n").each do |cookie|
-
cookie_name, cookie_filling = cookie.split('=', 2)
-
cookie_attributes = identify_cookie_attributes cookie_filling
-
parsed_cookie = CGI::Cookie.new(
-
'name' => cookie_name.strip,
-
'value' => cookie_attributes.fetch('value'),
-
'path' => cookie_attributes.fetch('path', nil),
-
'domain' => cookie_attributes.fetch('domain', nil),
-
'expires' => cookie_attributes.fetch('expires', nil),
-
'secure' => cookie_attributes.fetch('secure', false)
-
)
-
cookies.store(cookie_name, parsed_cookie)
-
end
-
end
-
2
cookies
-
end
-
-
1
def identify_cookie_attributes(cookie_filling)
-
cookie_bits = cookie_filling.split(';')
-
cookie_attributes = Hash.new
-
cookie_attributes.store('value', cookie_bits[0].strip)
-
cookie_bits.each do |bit|
-
if bit.include? '='
-
cookie_attribute, attribute_value = bit.split('=')
-
cookie_attributes.store(cookie_attribute.strip, attribute_value.strip)
-
if cookie_attribute.include? 'max-age'
-
cookie_attributes.store('expires', Time.now + attribute_value.strip.to_i)
-
end
-
end
-
if bit.include? 'secure'
-
cookie_attributes.store('secure', true)
-
end
-
end
-
cookie_attributes
-
end
-
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Rack
-
1
class NullLogger
-
1
def initialize(app)
-
2
@app = app
-
end
-
-
1
def call(env)
-
2
env[RACK_LOGGER] = self
-
2
@app.call(env)
-
end
-
-
1
def info(progname = nil, &block); end
-
1
def debug(progname = nil, &block); end
-
1
def warn(progname = nil, &block); end
-
1
def error(progname = nil, &block); end
-
1
def fatal(progname = nil, &block); end
-
1
def unknown(progname = nil, &block); end
-
1
def info? ; end
-
1
def debug? ; end
-
1
def warn? ; end
-
1
def error? ; end
-
1
def fatal? ; end
-
1
def level ; end
-
1
def progname ; end
-
1
def datetime_format ; end
-
1
def formatter ; end
-
1
def sev_threshold ; end
-
1
def level=(level); end
-
1
def progname=(progname); end
-
1
def datetime_format=(datetime_format); end
-
1
def formatter=(formatter); end
-
1
def sev_threshold=(sev_threshold); end
-
1
def close ; end
-
1
def add(severity, message = nil, progname = nil, &block); end
-
1
def <<(msg); end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Rack
-
1
class QueryParser
-
1
(require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
-
-
1
DEFAULT_SEP = /[&;] */n
-
1
COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
-
-
# ParameterTypeError is the error that is raised when incoming structural
-
# parameters (parsed by parse_nested_query) contain conflicting types.
-
1
class ParameterTypeError < TypeError; end
-
-
# InvalidParameterError is the error that is raised when incoming structural
-
# parameters (parsed by parse_nested_query) contain invalid format or byte
-
# sequence.
-
1
class InvalidParameterError < ArgumentError; end
-
-
1
def self.make_default(key_space_limit, param_depth_limit)
-
1
new Params, key_space_limit, param_depth_limit
-
end
-
-
1
attr_reader :key_space_limit, :param_depth_limit
-
-
1
def initialize(params_class, key_space_limit, param_depth_limit)
-
1
@params_class = params_class
-
1
@key_space_limit = key_space_limit
-
1
@param_depth_limit = param_depth_limit
-
end
-
-
# Stolen from Mongrel, with some small modifications:
-
# Parses a query string by breaking it up at the '&'
-
# and ';' characters. You can also use this to parse
-
# cookies by changing the characters used in the second
-
# parameter (which defaults to '&;').
-
1
def parse_query(qs, d = nil, &unescaper)
-
unescaper ||= method(:unescape)
-
-
params = make_params
-
-
(qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
-
next if p.empty?
-
k, v = p.split('=', 2).map!(&unescaper)
-
-
if cur = params[k]
-
if cur.class == Array
-
params[k] << v
-
else
-
params[k] = [cur, v]
-
end
-
else
-
params[k] = v
-
end
-
end
-
-
return params.to_h
-
end
-
-
# parse_nested_query expands a query string into structural types. Supported
-
# types are Arrays, Hashes and basic value types. It is possible to supply
-
# query strings with parameters of conflicting types, in this case a
-
# ParameterTypeError is raised. Users are encouraged to return a 400 in this
-
# case.
-
1
def parse_nested_query(qs, d = nil)
-
2
params = make_params
-
-
2
unless qs.nil? || qs.empty?
-
(qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
-
k, v = p.split('=', 2).map! { |s| unescape(s) }
-
-
normalize_params(params, k, v, param_depth_limit)
-
end
-
end
-
-
2
return params.to_h
-
rescue ArgumentError => e
-
raise InvalidParameterError, e.message, e.backtrace
-
end
-
-
# normalize_params recursively expands parameters into structural types. If
-
# the structural types represented by two different parameter names are in
-
# conflict, a ParameterTypeError is raised.
-
1
def normalize_params(params, name, v, depth)
-
raise RangeError if depth <= 0
-
-
name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
-
k = $1 || ''
-
after = $' || ''
-
-
if k.empty?
-
if !v.nil? && name == "[]"
-
return Array(v)
-
else
-
return
-
end
-
end
-
-
if after == ''
-
params[k] = v
-
elsif after == "["
-
params[name] = v
-
elsif after == "[]"
-
params[k] ||= []
-
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
-
params[k] << v
-
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
-
child_key = $1
-
params[k] ||= []
-
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
-
if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key)
-
normalize_params(params[k].last, child_key, v, depth - 1)
-
else
-
params[k] << normalize_params(make_params, child_key, v, depth - 1)
-
end
-
else
-
params[k] ||= make_params
-
raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
-
params[k] = normalize_params(params[k], after, v, depth - 1)
-
end
-
-
params
-
end
-
-
1
def make_params
-
2
@params_class.new @key_space_limit
-
end
-
-
1
def new_space_limit(key_space_limit)
-
self.class.new @params_class, key_space_limit, param_depth_limit
-
end
-
-
1
def new_depth_limit(param_depth_limit)
-
self.class.new @params_class, key_space_limit, param_depth_limit
-
end
-
-
1
private
-
-
1
def params_hash_type?(obj)
-
obj.kind_of?(@params_class)
-
end
-
-
1
def params_hash_has_key?(hash, key)
-
return false if /\[\]/.match?(key)
-
-
key.split(/[\[\]]+/).inject(hash) do |h, part|
-
next h if part == ''
-
return false unless params_hash_type?(h) && h.key?(part)
-
h[part]
-
end
-
-
true
-
end
-
-
1
def unescape(s)
-
Utils.unescape(s)
-
end
-
-
1
class Params
-
1
def initialize(limit)
-
2
@limit = limit
-
2
@size = 0
-
2
@params = {}
-
end
-
-
1
def [](key)
-
@params[key]
-
end
-
-
1
def []=(key, value)
-
@size += key.size if key && !@params.key?(key)
-
raise RangeError, 'exceeded available parameter key space' if @size > @limit
-
@params[key] = value
-
end
-
-
1
def key?(key)
-
@params.key?(key)
-
end
-
-
# Recursively unwraps nested `Params` objects and constructs an object
-
# of the same shape, but using the objects' internal representations
-
# (Ruby hashes) in place of the objects. The result is a hash consisting
-
# purely of Ruby primitives.
-
#
-
# Mutation warning!
-
#
-
# 1. This method mutates the internal representation of the `Params`
-
# objects in order to save object allocations.
-
#
-
# 2. The value you get back is a reference to the internal hash
-
# representation, not a copy.
-
#
-
# 3. Because the `Params` object's internal representation is mutable
-
# through the `#[]=` method, it is not thread safe. The result of
-
# getting the hash representation while another thread is adding a
-
# key to it is non-deterministic.
-
#
-
1
def to_h
-
2
@params.each do |key, value|
-
case value
-
when self
-
# Handle circular references gracefully.
-
@params[key] = @params
-
when Params
-
@params[key] = value.to_h
-
when Array
-
value.map! { |v| v.kind_of?(Params) ? v.to_h : v }
-
else
-
# Ignore anything that is not a `Params` object or
-
# a collection that can contain one.
-
end
-
end
-
2
@params
-
end
-
1
alias_method :to_params_hash, :to_h
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Rack
-
# Rack::Request provides a convenient interface to a Rack
-
# environment. It is stateless, the environment +env+ passed to the
-
# constructor will be directly modified.
-
#
-
# req = Rack::Request.new(env)
-
# req.post?
-
# req.params["data"]
-
-
1
class Request
-
1
(require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
-
-
1
class << self
-
1
attr_accessor :ip_filter
-
end
-
-
1
self.ip_filter = lambda { |ip| /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i.match?(ip) }
-
1
ALLOWED_SCHEMES = %w(https http).freeze
-
1
SCHEME_WHITELIST = ALLOWED_SCHEMES
-
1
if Object.respond_to?(:deprecate_constant)
-
1
deprecate_constant :SCHEME_WHITELIST
-
end
-
-
1
def initialize(env)
-
6
@params = nil
-
6
super(env)
-
end
-
-
1
def params
-
2
@params ||= super
-
end
-
-
1
def update_param(k, v)
-
super
-
@params = nil
-
end
-
-
1
def delete_param(k)
-
v = super
-
@params = nil
-
v
-
end
-
-
1
module Env
-
# The environment of the request.
-
1
attr_reader :env
-
-
1
def initialize(env)
-
6
@env = env
-
6
super()
-
end
-
-
# Predicate method to test to see if `name` has been set as request
-
# specific data
-
1
def has_header?(name)
-
@env.key? name
-
end
-
-
# Get a request specific value for `name`.
-
1
def get_header(name)
-
47
@env[name]
-
end
-
-
# If a block is given, it yields to the block if the value hasn't been set
-
# on the request.
-
1
def fetch_header(name, &block)
-
@env.fetch(name, &block)
-
end
-
-
# Loops through each key / value pair in the request specific data.
-
1
def each_header(&block)
-
@env.each(&block)
-
end
-
-
# Set a request specific value for `name` to `v`
-
1
def set_header(name, v)
-
4
@env[name] = v
-
end
-
-
# Add a header that may have multiple values.
-
#
-
# Example:
-
# request.add_header 'Accept', 'image/png'
-
# request.add_header 'Accept', '*/*'
-
#
-
# assert_equal 'image/png,*/*', request.get_header('Accept')
-
#
-
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
-
1
def add_header(key, v)
-
if v.nil?
-
get_header key
-
elsif has_header? key
-
set_header key, "#{get_header key},#{v}"
-
else
-
set_header key, v
-
end
-
end
-
-
# Delete a request specific value for `name`.
-
1
def delete_header(name)
-
@env.delete name
-
end
-
-
1
def initialize_copy(other)
-
@env = other.env.dup
-
end
-
end
-
-
1
module Helpers
-
# The set of form-data media-types. Requests that do not indicate
-
# one of the media types present in this list will not be eligible
-
# for form-data / param parsing.
-
1
FORM_DATA_MEDIA_TYPES = [
-
'application/x-www-form-urlencoded',
-
'multipart/form-data'
-
]
-
-
# The set of media-types. Requests that do not indicate
-
# one of the media types present in this list will not be eligible
-
# for param parsing like soap attachments or generic multiparts
-
1
PARSEABLE_DATA_MEDIA_TYPES = [
-
'multipart/related',
-
'multipart/mixed'
-
]
-
-
# Default ports depending on scheme. Used to decide whether or not
-
# to include the port in a generated URI.
-
1
DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
-
-
# The address of the client which connected to the proxy.
-
1
HTTP_X_FORWARDED_FOR = 'HTTP_X_FORWARDED_FOR'
-
-
# The contents of the host/:authority header sent to the proxy.
-
1
HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST'
-
-
# The value of the scheme sent to the proxy.
-
1
HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'
-
-
# The protocol used to connect to the proxy.
-
1
HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'
-
-
# The port used to connect to the proxy.
-
1
HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT'
-
-
# Another way for specifing https scheme was used.
-
1
HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL'
-
-
1
def body; get_header(RACK_INPUT) end
-
1
def script_name; get_header(SCRIPT_NAME).to_s end
-
1
def script_name=(s); set_header(SCRIPT_NAME, s.to_s) end
-
-
16
def path_info; get_header(PATH_INFO).to_s end
-
1
def path_info=(s); set_header(PATH_INFO, s.to_s) end
-
-
9
def request_method; get_header(REQUEST_METHOD) end
-
7
def query_string; get_header(QUERY_STRING).to_s end
-
1
def content_length; get_header('CONTENT_LENGTH') end
-
1
def logger; get_header(RACK_LOGGER) end
-
1
def user_agent; get_header('HTTP_USER_AGENT') end
-
1
def multithread?; get_header(RACK_MULTITHREAD) end
-
-
# the referer of the client
-
1
def referer; get_header('HTTP_REFERER') end
-
1
alias referrer referer
-
-
1
def session
-
fetch_header(RACK_SESSION) do |k|
-
set_header RACK_SESSION, default_session
-
end
-
end
-
-
1
def session_options
-
fetch_header(RACK_SESSION_OPTIONS) do |k|
-
set_header RACK_SESSION_OPTIONS, {}
-
end
-
end
-
-
# Checks the HTTP request method (or verb) to see if it was of type DELETE
-
1
def delete?; request_method == DELETE end
-
-
# Checks the HTTP request method (or verb) to see if it was of type GET
-
1
def get?; request_method == GET end
-
-
# Checks the HTTP request method (or verb) to see if it was of type HEAD
-
3
def head?; request_method == HEAD end
-
-
# Checks the HTTP request method (or verb) to see if it was of type OPTIONS
-
1
def options?; request_method == OPTIONS end
-
-
# Checks the HTTP request method (or verb) to see if it was of type LINK
-
1
def link?; request_method == LINK end
-
-
# Checks the HTTP request method (or verb) to see if it was of type PATCH
-
1
def patch?; request_method == PATCH end
-
-
# Checks the HTTP request method (or verb) to see if it was of type POST
-
1
def post?; request_method == POST end
-
-
# Checks the HTTP request method (or verb) to see if it was of type PUT
-
1
def put?; request_method == PUT end
-
-
# Checks the HTTP request method (or verb) to see if it was of type TRACE
-
1
def trace?; request_method == TRACE end
-
-
# Checks the HTTP request method (or verb) to see if it was of type UNLINK
-
1
def unlink?; request_method == UNLINK end
-
-
1
def scheme
-
if get_header(HTTPS) == 'on'
-
'https'
-
elsif get_header(HTTP_X_FORWARDED_SSL) == 'on'
-
'https'
-
elsif forwarded_scheme
-
forwarded_scheme
-
else
-
get_header(RACK_URL_SCHEME)
-
end
-
end
-
-
# The authority of the incoming request as defined by RFC3976.
-
# https://tools.ietf.org/html/rfc3986#section-3.2
-
#
-
# In HTTP/1, this is the `host` header.
-
# In HTTP/2, this is the `:authority` pseudo-header.
-
1
def authority
-
forwarded_authority || host_authority || server_authority
-
end
-
-
# The authority as defined by the `SERVER_NAME` and `SERVER_PORT`
-
# variables.
-
1
def server_authority
-
host = self.server_name
-
port = self.server_port
-
-
if host
-
if port
-
"#{host}:#{port}"
-
else
-
host
-
end
-
end
-
end
-
-
1
def server_name
-
get_header(SERVER_NAME)
-
end
-
-
1
def server_port
-
if port = get_header(SERVER_PORT)
-
Integer(port)
-
end
-
end
-
-
1
def cookies
-
hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |key|
-
set_header(key, {})
-
end
-
-
string = get_header(HTTP_COOKIE)
-
-
unless string == get_header(RACK_REQUEST_COOKIE_STRING)
-
hash.replace Utils.parse_cookies_header(string)
-
set_header(RACK_REQUEST_COOKIE_STRING, string)
-
end
-
-
hash
-
end
-
-
1
def content_type
-
4
content_type = get_header('CONTENT_TYPE')
-
4
content_type.nil? || content_type.empty? ? nil : content_type
-
end
-
-
1
def xhr?
-
2
get_header("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest"
-
end
-
-
# The `HTTP_HOST` header.
-
1
def host_authority
-
get_header(HTTP_HOST)
-
end
-
-
1
def host_with_port(authority = self.authority)
-
host, _, port = split_authority(authority)
-
-
if port == DEFAULT_PORTS[self.scheme]
-
host
-
else
-
authority
-
end
-
end
-
-
# Returns a formatted host, suitable for being used in a URI.
-
1
def host
-
split_authority(self.authority)[0]
-
end
-
-
# Returns an address suitable for being to resolve to an address.
-
# In the case of a domain name or IPv4 address, the result is the same
-
# as +host+. In the case of IPv6 or future address formats, the square
-
# brackets are removed.
-
1
def hostname
-
split_authority(self.authority)[1]
-
end
-
-
1
def port
-
if authority = self.authority
-
_, _, port = split_authority(self.authority)
-
-
if port
-
return port
-
end
-
end
-
-
if forwarded_port = self.forwarded_port
-
return forwarded_port.first
-
end
-
-
if scheme = self.scheme
-
if port = DEFAULT_PORTS[self.scheme]
-
return port
-
end
-
end
-
-
self.server_port
-
end
-
-
1
def forwarded_for
-
if value = get_header(HTTP_X_FORWARDED_FOR)
-
split_header(value).map do |authority|
-
split_authority(wrap_ipv6(authority))[1]
-
end
-
end
-
end
-
-
1
def forwarded_port
-
if value = get_header(HTTP_X_FORWARDED_PORT)
-
split_header(value).map(&:to_i)
-
end
-
end
-
-
1
def forwarded_authority
-
if value = get_header(HTTP_X_FORWARDED_HOST)
-
wrap_ipv6(split_header(value).first)
-
end
-
end
-
-
1
def ssl?
-
scheme == 'https' || scheme == 'wss'
-
end
-
-
1
def ip
-
remote_addresses = split_header(get_header('REMOTE_ADDR'))
-
external_addresses = reject_trusted_ip_addresses(remote_addresses)
-
-
unless external_addresses.empty?
-
return external_addresses.first
-
end
-
-
if forwarded_for = self.forwarded_for
-
unless forwarded_for.empty?
-
# The forwarded for addresses are ordered: client, proxy1, proxy2.
-
# So we reject all the trusted addresses (proxy*) and return the
-
# last client. Or if we trust everyone, we just return the first
-
# address.
-
return reject_trusted_ip_addresses(forwarded_for).last || forwarded_for.first
-
end
-
end
-
-
# If all the addresses are trusted, and we aren't forwarded, just return
-
# the first remote address, which represents the source of the request.
-
remote_addresses.first
-
end
-
-
# The media type (type/subtype) portion of the CONTENT_TYPE header
-
# without any media type parameters. e.g., when CONTENT_TYPE is
-
# "text/plain;charset=utf-8", the media-type is "text/plain".
-
#
-
# For more information on the use of media types in HTTP, see:
-
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
-
1
def media_type
-
4
MediaType.type(content_type)
-
end
-
-
# The media type parameters provided in CONTENT_TYPE as a Hash, or
-
# an empty Hash if no CONTENT_TYPE or media-type parameters were
-
# provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
-
# this method responds with the following Hash:
-
# { 'charset' => 'utf-8' }
-
1
def media_type_params
-
MediaType.params(content_type)
-
end
-
-
# The character set of the request body if a "charset" media type
-
# parameter was given, or nil if no "charset" was specified. Note
-
# that, per RFC2616, text/* media types that specify no explicit
-
# charset are to be considered ISO-8859-1.
-
1
def content_charset
-
media_type_params['charset']
-
end
-
-
# Determine whether the request body contains form-data by checking
-
# the request Content-Type for one of the media-types:
-
# "application/x-www-form-urlencoded" or "multipart/form-data". The
-
# list of form-data media types can be modified through the
-
# +FORM_DATA_MEDIA_TYPES+ array.
-
#
-
# A request body is also assumed to contain form-data when no
-
# Content-Type header is provided and the request_method is POST.
-
1
def form_data?
-
2
type = media_type
-
2
meth = get_header(RACK_METHODOVERRIDE_ORIGINAL_METHOD) || get_header(REQUEST_METHOD)
-
-
2
(meth == POST && type.nil?) || FORM_DATA_MEDIA_TYPES.include?(type)
-
end
-
-
# Determine whether the request body contains data by checking
-
# the request media_type against registered parse-data media-types
-
1
def parseable_data?
-
2
PARSEABLE_DATA_MEDIA_TYPES.include?(media_type)
-
end
-
-
# Returns the data received in the query string.
-
1
def GET
-
2
if get_header(RACK_REQUEST_QUERY_STRING) == query_string
-
get_header(RACK_REQUEST_QUERY_HASH)
-
else
-
2
query_hash = parse_query(query_string, '&;')
-
2
set_header(RACK_REQUEST_QUERY_STRING, query_string)
-
2
set_header(RACK_REQUEST_QUERY_HASH, query_hash)
-
end
-
end
-
-
# Returns the data received in the request body.
-
#
-
# This method support both application/x-www-form-urlencoded and
-
# multipart/form-data.
-
1
def POST
-
2
if get_header(RACK_INPUT).nil?
-
raise "Missing rack.input"
-
2
elsif get_header(RACK_REQUEST_FORM_INPUT) == get_header(RACK_INPUT)
-
get_header(RACK_REQUEST_FORM_HASH)
-
2
elsif form_data? || parseable_data?
-
unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
-
form_vars = get_header(RACK_INPUT).read
-
-
# Fix for Safari Ajax postings that always append \0
-
# form_vars.sub!(/\0\z/, '') # performance replacement:
-
form_vars.slice!(-1) if form_vars.end_with?("\0")
-
-
set_header RACK_REQUEST_FORM_VARS, form_vars
-
set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&')
-
-
get_header(RACK_INPUT).rewind
-
end
-
set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
-
get_header RACK_REQUEST_FORM_HASH
-
else
-
2
{}
-
end
-
end
-
-
# The union of GET and POST data.
-
#
-
# Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
-
1
def params
-
2
self.GET.merge(self.POST)
-
end
-
-
# Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
-
#
-
# The parameter is updated wherever it was previous defined, so GET, POST, or both. If it wasn't previously defined, it's inserted into GET.
-
#
-
# <tt>env['rack.input']</tt> is not touched.
-
1
def update_param(k, v)
-
found = false
-
if self.GET.has_key?(k)
-
found = true
-
self.GET[k] = v
-
end
-
if self.POST.has_key?(k)
-
found = true
-
self.POST[k] = v
-
end
-
unless found
-
self.GET[k] = v
-
end
-
end
-
-
# Destructively delete a parameter, whether it's in GET or POST. Returns the value of the deleted parameter.
-
#
-
# If the parameter is in both GET and POST, the POST value takes precedence since that's how #params works.
-
#
-
# <tt>env['rack.input']</tt> is not touched.
-
1
def delete_param(k)
-
post_value, get_value = self.POST.delete(k), self.GET.delete(k)
-
post_value || get_value
-
end
-
-
1
def base_url
-
"#{scheme}://#{host_with_port}"
-
end
-
-
# Tries to return a remake of the original request URL as a string.
-
1
def url
-
base_url + fullpath
-
end
-
-
1
def path
-
script_name + path_info
-
end
-
-
1
def fullpath
-
query_string.empty? ? path : "#{path}?#{query_string}"
-
end
-
-
1
def accept_encoding
-
parse_http_accept_header(get_header("HTTP_ACCEPT_ENCODING"))
-
end
-
-
1
def accept_language
-
parse_http_accept_header(get_header("HTTP_ACCEPT_LANGUAGE"))
-
end
-
-
1
def trusted_proxy?(ip)
-
Rack::Request.ip_filter.call(ip)
-
end
-
-
# shortcut for <tt>request.params[key]</tt>
-
1
def [](key)
-
if $VERBOSE
-
warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead")
-
end
-
-
params[key.to_s]
-
end
-
-
# shortcut for <tt>request.params[key] = value</tt>
-
#
-
# Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
-
1
def []=(key, value)
-
if $VERBOSE
-
warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead")
-
end
-
-
params[key.to_s] = value
-
end
-
-
# like Hash#values_at
-
1
def values_at(*keys)
-
keys.map { |key| params[key] }
-
end
-
-
1
private
-
-
1
def default_session; {}; end
-
-
# Assist with compatibility when processing `X-Forwarded-For`.
-
1
def wrap_ipv6(host)
-
# Even thought IPv6 addresses should be wrapped in square brackets,
-
# sometimes this is not done in various legacy/underspecified headers.
-
# So we try to fix this situation for compatibility reasons.
-
-
# Try to detect IPv6 addresses which aren't escaped yet:
-
if !host.start_with?('[') && host.count(':') > 1
-
"[#{host}]"
-
else
-
host
-
end
-
end
-
-
1
def parse_http_accept_header(header)
-
header.to_s.split(/\s*,\s*/).map do |part|
-
attribute, parameters = part.split(/\s*;\s*/, 2)
-
quality = 1.0
-
if parameters and /\Aq=([\d.]+)/ =~ parameters
-
quality = $1.to_f
-
end
-
[attribute, quality]
-
end
-
end
-
-
1
def query_parser
-
2
Utils.default_query_parser
-
end
-
-
1
def parse_query(qs, d = '&')
-
2
query_parser.parse_nested_query(qs, d)
-
end
-
-
1
def parse_multipart
-
Rack::Multipart.extract_multipart(self, query_parser)
-
end
-
-
1
def split_header(value)
-
value ? value.strip.split(/[,\s]+/) : []
-
end
-
-
1
AUTHORITY = /^
-
# The host:
-
(?<host>
-
# An IPv6 address:
-
(\[(?<ip6>.*)\])
-
|
-
# An IPv4 address:
-
(?<ip4>[\d\.]+)
-
|
-
# A hostname:
-
(?<name>[a-zA-Z0-9\.\-]+)
-
)
-
# The optional port:
-
(:(?<port>\d+))?
-
$/x
-
-
1
private_constant :AUTHORITY
-
-
1
def split_authority(authority)
-
if match = AUTHORITY.match(authority)
-
if address = match[:ip6]
-
return match[:host], address, match[:port]&.to_i
-
else
-
return match[:host], match[:host], match[:port]&.to_i
-
end
-
end
-
-
# Give up!
-
return authority, authority, nil
-
end
-
-
1
def reject_trusted_ip_addresses(ip_addresses)
-
ip_addresses.reject { |ip| trusted_proxy?(ip) }
-
end
-
-
1
def forwarded_scheme
-
allowed_scheme(get_header(HTTP_X_FORWARDED_SCHEME)) ||
-
allowed_scheme(extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO)))
-
end
-
-
1
def allowed_scheme(header)
-
header if ALLOWED_SCHEMES.include?(header)
-
end
-
-
1
def extract_proto_header(header)
-
if header
-
if (comma_index = header.index(','))
-
header[0, comma_index]
-
else
-
header
-
end
-
end
-
end
-
end
-
-
1
include Env
-
1
include Helpers
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require 'time'
-
-
1
module Rack
-
# Rack::Response provides a convenient interface to create a Rack
-
# response.
-
#
-
# It allows setting of headers and cookies, and provides useful
-
# defaults (an OK response with empty headers and body).
-
#
-
# You can use Response#write to iteratively generate your response,
-
# but note that this is buffered by Rack::Response until you call
-
# +finish+. +finish+ however can take a block inside which calls to
-
# +write+ are synchronous with the Rack response.
-
#
-
# Your application's +call+ should end returning Response#finish.
-
1
class Response
-
1
def self.[](status, headers, body)
-
self.new(body, status, headers)
-
end
-
-
1
CHUNKED = 'chunked'
-
1
STATUS_WITH_NO_ENTITY_BODY = Utils::STATUS_WITH_NO_ENTITY_BODY
-
-
1
attr_accessor :length, :status, :body
-
1
attr_reader :headers
-
-
# @deprecated Use {#headers} instead.
-
1
alias header headers
-
-
# Initialize the response object with the specified body, status
-
# and headers.
-
#
-
# @param body [nil, #each, #to_str] the response body.
-
# @param status [Integer] the integer status as defined by the
-
# HTTP protocol RFCs.
-
# @param headers [#each] a list of key-value header pairs which
-
# conform to the HTTP protocol RFCs.
-
#
-
# Providing a body which responds to #to_str is legacy behaviour.
-
1
def initialize(body = nil, status = 200, headers = {})
-
4
@status = status.to_i
-
4
@headers = Utils::HeaderHash[headers]
-
-
4
@writer = self.method(:append)
-
-
4
@block = nil
-
-
# Keep track of whether we have expanded the user supplied body.
-
4
if body.nil?
-
2
@body = []
-
2
@buffered = true
-
2
@length = 0
-
2
elsif body.respond_to?(:to_str)
-
@body = [body]
-
@buffered = true
-
@length = body.to_str.bytesize
-
else
-
2
@body = body
-
2
@buffered = false
-
2
@length = 0
-
end
-
-
4
yield self if block_given?
-
end
-
-
1
def redirect(target, status = 302)
-
self.status = status
-
self.location = target
-
end
-
-
1
def chunked?
-
2
CHUNKED == get_header(TRANSFER_ENCODING)
-
end
-
-
# Generate a response array consistent with the requirements of the SPEC.
-
# @return [Array] a 3-tuple suitable of `[status, headers, body]`
-
# which is suitable to be returned from the middleware `#call(env)` method.
-
1
def finish(&block)
-
2
if STATUS_WITH_NO_ENTITY_BODY[status.to_i]
-
delete_header CONTENT_TYPE
-
delete_header CONTENT_LENGTH
-
close
-
return [@status, @headers, []]
-
else
-
2
if block_given?
-
@block = block
-
return [@status, @headers, self]
-
else
-
2
return [@status, @headers, @body]
-
end
-
end
-
end
-
-
1
alias to_a finish # For *response
-
-
1
def each(&callback)
-
@body.each(&callback)
-
@buffered = true
-
-
if @block
-
@writer = callback
-
@block.call(self)
-
end
-
end
-
-
# Append to body and update Content-Length.
-
#
-
# NOTE: Do not mix #write and direct #body access!
-
#
-
1
def write(chunk)
-
buffered_body!
-
-
@writer.call(chunk.to_s)
-
end
-
-
1
def close
-
@body.close if @body.respond_to?(:close)
-
end
-
-
1
def empty?
-
@block == nil && @body.empty?
-
end
-
-
1
def has_header?(key); headers.key? key; end
-
9
def get_header(key); headers[key]; end
-
9
def set_header(key, v); headers[key] = v; end
-
1
def delete_header(key); headers.delete key; end
-
-
1
alias :[] :get_header
-
1
alias :[]= :set_header
-
-
1
module Helpers
-
1
def invalid?; status < 100 || status >= 600; end
-
-
1
def informational?; status >= 100 && status < 200; end
-
1
def successful?; status >= 200 && status < 300; end
-
1
def redirection?; status >= 300 && status < 400; end
-
1
def client_error?; status >= 400 && status < 500; end
-
1
def server_error?; status >= 500 && status < 600; end
-
-
1
def ok?; status == 200; end
-
1
def created?; status == 201; end
-
1
def accepted?; status == 202; end
-
1
def no_content?; status == 204; end
-
1
def moved_permanently?; status == 301; end
-
1
def bad_request?; status == 400; end
-
1
def unauthorized?; status == 401; end
-
1
def forbidden?; status == 403; end
-
1
def not_found?; status == 404; end
-
1
def method_not_allowed?; status == 405; end
-
1
def precondition_failed?; status == 412; end
-
1
def unprocessable?; status == 422; end
-
-
1
def redirect?; [301, 302, 303, 307, 308].include? status; end
-
-
1
def include?(header)
-
has_header? header
-
end
-
-
# Add a header that may have multiple values.
-
#
-
# Example:
-
# response.add_header 'Vary', 'Accept-Encoding'
-
# response.add_header 'Vary', 'Cookie'
-
#
-
# assert_equal 'Accept-Encoding,Cookie', response.get_header('Vary')
-
#
-
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
-
1
def add_header(key, v)
-
if v.nil?
-
get_header key
-
elsif has_header? key
-
set_header key, "#{get_header key},#{v}"
-
else
-
set_header key, v
-
end
-
end
-
-
# Get the content type of the response.
-
1
def content_type
-
get_header CONTENT_TYPE
-
end
-
-
# Set the content type of the response.
-
1
def content_type=(content_type)
-
set_header CONTENT_TYPE, content_type
-
end
-
-
1
def media_type
-
MediaType.type(content_type)
-
end
-
-
1
def media_type_params
-
MediaType.params(content_type)
-
end
-
-
1
def content_length
-
cl = get_header CONTENT_LENGTH
-
cl ? cl.to_i : cl
-
end
-
-
1
def location
-
get_header "Location"
-
end
-
-
1
def location=(location)
-
set_header "Location", location
-
end
-
-
1
def set_cookie(key, value)
-
cookie_header = get_header SET_COOKIE
-
set_header SET_COOKIE, ::Rack::Utils.add_cookie_to_header(cookie_header, key, value)
-
end
-
-
1
def delete_cookie(key, value = {})
-
set_header SET_COOKIE, ::Rack::Utils.add_remove_cookie_to_header(get_header(SET_COOKIE), key, value)
-
end
-
-
1
def set_cookie_header
-
get_header SET_COOKIE
-
end
-
-
1
def set_cookie_header=(v)
-
set_header SET_COOKIE, v
-
end
-
-
1
def cache_control
-
get_header CACHE_CONTROL
-
end
-
-
1
def cache_control=(v)
-
set_header CACHE_CONTROL, v
-
end
-
-
# Specifies that the content shouldn't be cached. Overrides `cache!` if already called.
-
1
def do_not_cache!
-
set_header CACHE_CONTROL, "no-cache, must-revalidate"
-
set_header EXPIRES, Time.now.httpdate
-
end
-
-
# Specify that the content should be cached.
-
# @param duration [Integer] The number of seconds until the cache expires.
-
# @option directive [String] The cache control directive, one of "public", "private", "no-cache" or "no-store".
-
1
def cache!(duration = 3600, directive: "public")
-
unless headers[CACHE_CONTROL] =~ /no-cache/
-
set_header CACHE_CONTROL, "#{directive}, max-age=#{duration}"
-
set_header EXPIRES, (Time.now + duration).httpdate
-
end
-
end
-
-
1
def etag
-
get_header ETAG
-
end
-
-
1
def etag=(v)
-
set_header ETAG, v
-
end
-
-
1
protected
-
-
1
def buffered_body!
-
2
return if @buffered
-
-
2
if @body.is_a?(Array)
-
# The user supplied body was an array:
-
@body = @body.compact
-
@body.each do |part|
-
@length += part.to_s.bytesize
-
end
-
else
-
# Turn the user supplied body into a buffered array:
-
2
body = @body
-
2
@body = Array.new
-
-
2
body.each do |part|
-
2
@writer.call(part.to_s)
-
end
-
-
2
body.close if body.respond_to?(:close)
-
end
-
-
2
@buffered = true
-
end
-
-
1
def append(chunk)
-
2
@body << chunk
-
-
2
unless chunked?
-
2
@length += chunk.bytesize
-
2
set_header(CONTENT_LENGTH, @length.to_s)
-
end
-
-
2
return chunk
-
end
-
end
-
-
1
include Helpers
-
-
1
class Raw
-
1
include Helpers
-
-
1
attr_reader :headers
-
1
attr_accessor :status
-
-
1
def initialize(status, headers)
-
@status = status
-
@headers = headers
-
end
-
-
1
def has_header?(key); headers.key? key; end
-
1
def get_header(key); headers[key]; end
-
1
def set_header(key, v); headers[key] = v; end
-
1
def delete_header(key); headers.delete key; end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
-
# bugrep: Andreas Zehnder
-
-
1
require_relative '../../../rack'
-
1
require 'time'
-
1
require 'securerandom'
-
1
require 'digest/sha2'
-
-
1
module Rack
-
-
1
module Session
-
-
1
class SessionId
-
1
ID_VERSION = 2
-
-
1
attr_reader :public_id
-
-
1
def initialize(public_id)
-
@public_id = public_id
-
end
-
-
1
def private_id
-
"#{ID_VERSION}::#{hash_sid(public_id)}"
-
end
-
-
1
alias :cookie_value :public_id
-
1
alias :to_s :public_id
-
-
1
def empty?; false; end
-
1
def inspect; public_id.inspect; end
-
-
1
private
-
-
1
def hash_sid(sid)
-
Digest::SHA256.hexdigest(sid)
-
end
-
end
-
-
1
module Abstract
-
# SessionHash is responsible to lazily load the session from store.
-
-
1
class SessionHash
-
1
include Enumerable
-
1
attr_writer :id
-
-
1
Unspecified = Object.new
-
-
1
def self.find(req)
-
req.get_header RACK_SESSION
-
end
-
-
1
def self.set(req, session)
-
req.set_header RACK_SESSION, session
-
end
-
-
1
def self.set_options(req, options)
-
req.set_header RACK_SESSION_OPTIONS, options.dup
-
end
-
-
1
def initialize(store, req)
-
@store = store
-
@req = req
-
@loaded = false
-
end
-
-
1
def id
-
return @id if @loaded or instance_variable_defined?(:@id)
-
@id = @store.send(:extract_session_id, @req)
-
end
-
-
1
def options
-
@req.session_options
-
end
-
-
1
def each(&block)
-
load_for_read!
-
@data.each(&block)
-
end
-
-
1
def [](key)
-
load_for_read!
-
@data[key.to_s]
-
end
-
-
1
def dig(key, *keys)
-
load_for_read!
-
@data.dig(key.to_s, *keys)
-
end
-
-
1
def fetch(key, default = Unspecified, &block)
-
load_for_read!
-
if default == Unspecified
-
@data.fetch(key.to_s, &block)
-
else
-
@data.fetch(key.to_s, default, &block)
-
end
-
end
-
-
1
def has_key?(key)
-
load_for_read!
-
@data.has_key?(key.to_s)
-
end
-
1
alias :key? :has_key?
-
1
alias :include? :has_key?
-
-
1
def []=(key, value)
-
load_for_write!
-
@data[key.to_s] = value
-
end
-
1
alias :store :[]=
-
-
1
def clear
-
load_for_write!
-
@data.clear
-
end
-
-
1
def destroy
-
clear
-
@id = @store.send(:delete_session, @req, id, options)
-
end
-
-
1
def to_hash
-
load_for_read!
-
@data.dup
-
end
-
-
1
def update(hash)
-
load_for_write!
-
@data.update(stringify_keys(hash))
-
end
-
1
alias :merge! :update
-
-
1
def replace(hash)
-
load_for_write!
-
@data.replace(stringify_keys(hash))
-
end
-
-
1
def delete(key)
-
load_for_write!
-
@data.delete(key.to_s)
-
end
-
-
1
def inspect
-
if loaded?
-
@data.inspect
-
else
-
"#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>"
-
end
-
end
-
-
1
def exists?
-
return @exists if instance_variable_defined?(:@exists)
-
@data = {}
-
@exists = @store.send(:session_exists?, @req)
-
end
-
-
1
def loaded?
-
@loaded
-
end
-
-
1
def empty?
-
load_for_read!
-
@data.empty?
-
end
-
-
1
def keys
-
load_for_read!
-
@data.keys
-
end
-
-
1
def values
-
load_for_read!
-
@data.values
-
end
-
-
1
private
-
-
1
def load_for_read!
-
load! if !loaded? && exists?
-
end
-
-
1
def load_for_write!
-
load! unless loaded?
-
end
-
-
1
def load!
-
@id, session = @store.send(:load_session, @req)
-
@data = stringify_keys(session)
-
@loaded = true
-
end
-
-
1
def stringify_keys(other)
-
# Use transform_keys after dropping Ruby 2.4 support
-
hash = {}
-
other.to_hash.each do |key, value|
-
hash[key.to_s] = value
-
end
-
hash
-
end
-
end
-
-
# ID sets up a basic framework for implementing an id based sessioning
-
# service. Cookies sent to the client for maintaining sessions will only
-
# contain an id reference. Only #find_session, #write_session and
-
# #delete_session are required to be overwritten.
-
#
-
# All parameters are optional.
-
# * :key determines the name of the cookie, by default it is
-
# 'rack.session'
-
# * :path, :domain, :expire_after, :secure, and :httponly set the related
-
# cookie options as by Rack::Response#set_cookie
-
# * :skip will not a set a cookie in the response nor update the session state
-
# * :defer will not set a cookie in the response but still update the session
-
# state if it is used with a backend
-
# * :renew (implementation dependent) will prompt the generation of a new
-
# session id, and migration of data to be referenced at the new id. If
-
# :defer is set, it will be overridden and the cookie will be set.
-
# * :sidbits sets the number of bits in length that a generated session
-
# id will be.
-
#
-
# These options can be set on a per request basis, at the location of
-
# <tt>env['rack.session.options']</tt>. Additionally the id of the
-
# session can be found within the options hash at the key :id. It is
-
# highly not recommended to change its value.
-
#
-
# Is Rack::Utils::Context compatible.
-
#
-
# Not included by default; you must require 'rack/session/abstract/id'
-
# to use.
-
-
1
class Persisted
-
DEFAULT_OPTIONS = {
-
1
key: RACK_SESSION,
-
path: '/',
-
domain: nil,
-
expire_after: nil,
-
secure: false,
-
httponly: true,
-
defer: false,
-
renew: false,
-
sidbits: 128,
-
cookie_only: true,
-
secure_random: ::SecureRandom
-
}.freeze
-
-
1
attr_reader :key, :default_options, :sid_secure
-
-
1
def initialize(app, options = {})
-
@app = app
-
@default_options = self.class::DEFAULT_OPTIONS.merge(options)
-
@key = @default_options.delete(:key)
-
@cookie_only = @default_options.delete(:cookie_only)
-
@same_site = @default_options.delete(:same_site)
-
initialize_sid
-
end
-
-
1
def call(env)
-
context(env)
-
end
-
-
1
def context(env, app = @app)
-
req = make_request env
-
prepare_session(req)
-
status, headers, body = app.call(req.env)
-
res = Rack::Response::Raw.new status, headers
-
commit_session(req, res)
-
[status, headers, body]
-
end
-
-
1
private
-
-
1
def make_request(env)
-
Rack::Request.new env
-
end
-
-
1
def initialize_sid
-
@sidbits = @default_options[:sidbits]
-
@sid_secure = @default_options[:secure_random]
-
@sid_length = @sidbits / 4
-
end
-
-
# Generate a new session id using Ruby #rand. The size of the
-
# session id is controlled by the :sidbits option.
-
# Monkey patch this to use custom methods for session id generation.
-
-
1
def generate_sid(secure = @sid_secure)
-
if secure
-
secure.hex(@sid_length)
-
else
-
"%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1)
-
end
-
rescue NotImplementedError
-
generate_sid(false)
-
end
-
-
# Sets the lazy session at 'rack.session' and places options and session
-
# metadata into 'rack.session.options'.
-
-
1
def prepare_session(req)
-
session_was = req.get_header RACK_SESSION
-
session = session_class.new(self, req)
-
req.set_header RACK_SESSION, session
-
req.set_header RACK_SESSION_OPTIONS, @default_options.dup
-
session.merge! session_was if session_was
-
end
-
-
# Extracts the session id from provided cookies and passes it and the
-
# environment to #find_session.
-
-
1
def load_session(req)
-
sid = current_session_id(req)
-
sid, session = find_session(req, sid)
-
[sid, session || {}]
-
end
-
-
# Extract session id from request object.
-
-
1
def extract_session_id(request)
-
sid = request.cookies[@key]
-
sid ||= request.params[@key] unless @cookie_only
-
sid
-
end
-
-
# Returns the current session id from the SessionHash.
-
-
1
def current_session_id(req)
-
req.get_header(RACK_SESSION).id
-
end
-
-
# Check if the session exists or not.
-
-
1
def session_exists?(req)
-
value = current_session_id(req)
-
value && !value.empty?
-
end
-
-
# Session should be committed if it was loaded, any of specific options like :renew, :drop
-
# or :expire_after was given and the security permissions match. Skips if skip is given.
-
-
1
def commit_session?(req, session, options)
-
if options[:skip]
-
false
-
else
-
has_session = loaded_session?(session) || forced_session_update?(session, options)
-
has_session && security_matches?(req, options)
-
end
-
end
-
-
1
def loaded_session?(session)
-
!session.is_a?(session_class) || session.loaded?
-
end
-
-
1
def forced_session_update?(session, options)
-
force_options?(options) && session && !session.empty?
-
end
-
-
1
def force_options?(options)
-
options.values_at(:max_age, :renew, :drop, :defer, :expire_after).any?
-
end
-
-
1
def security_matches?(request, options)
-
return true unless options[:secure]
-
request.ssl?
-
end
-
-
# Acquires the session from the environment and the session id from
-
# the session options and passes them to #write_session. If successful
-
# and the :defer option is not true, a cookie will be added to the
-
# response with the session's id.
-
-
1
def commit_session(req, res)
-
session = req.get_header RACK_SESSION
-
options = session.options
-
-
if options[:drop] || options[:renew]
-
session_id = delete_session(req, session.id || generate_sid, options)
-
return unless session_id
-
end
-
-
return unless commit_session?(req, session, options)
-
-
session.send(:load!) unless loaded_session?(session)
-
session_id ||= session.id
-
session_data = session.to_hash.delete_if { |k, v| v.nil? }
-
-
if not data = write_session(req, session_id, session_data, options)
-
req.get_header(RACK_ERRORS).puts("Warning! #{self.class.name} failed to save session. Content dropped.")
-
elsif options[:defer] and not options[:renew]
-
req.get_header(RACK_ERRORS).puts("Deferring cookie for #{session_id}") if $VERBOSE
-
else
-
cookie = Hash.new
-
cookie[:value] = cookie_value(data)
-
cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
-
cookie[:expires] = Time.now + options[:max_age] if options[:max_age]
-
-
if @same_site.respond_to? :call
-
cookie[:same_site] = @same_site.call(req, res)
-
else
-
cookie[:same_site] = @same_site
-
end
-
set_cookie(req, res, cookie.merge!(options))
-
end
-
end
-
1
public :commit_session
-
-
1
def cookie_value(data)
-
data
-
end
-
-
# Sets the cookie back to the client with session id. We skip the cookie
-
# setting if the value didn't change (sid is the same) or expires was given.
-
-
1
def set_cookie(request, res, cookie)
-
if request.cookies[@key] != cookie[:value] || cookie[:expires]
-
res.set_cookie_header =
-
Utils.add_cookie_to_header(res.set_cookie_header, @key, cookie)
-
end
-
end
-
-
# Allow subclasses to prepare_session for different Session classes
-
-
1
def session_class
-
SessionHash
-
end
-
-
# All thread safety and session retrieval procedures should occur here.
-
# Should return [session_id, session].
-
# If nil is provided as the session id, generation of a new valid id
-
# should occur within.
-
-
1
def find_session(env, sid)
-
raise '#find_session not implemented.'
-
end
-
-
# All thread safety and session storage procedures should occur here.
-
# Must return the session id if the session was saved successfully, or
-
# false if the session could not be saved.
-
-
1
def write_session(req, sid, session, options)
-
raise '#write_session not implemented.'
-
end
-
-
# All thread safety and session destroy procedures should occur here.
-
# Should return a new session id or nil if options[:drop]
-
-
1
def delete_session(req, sid, options)
-
raise '#delete_session not implemented'
-
end
-
end
-
-
1
class PersistedSecure < Persisted
-
1
class SecureSessionHash < SessionHash
-
1
def [](key)
-
if key == "session_id"
-
load_for_read!
-
id.public_id if id
-
else
-
super
-
end
-
end
-
end
-
-
1
def generate_sid(*)
-
public_id = super
-
-
SessionId.new(public_id)
-
end
-
-
1
def extract_session_id(*)
-
public_id = super
-
public_id && SessionId.new(public_id)
-
end
-
-
1
private
-
-
1
def session_class
-
SecureSessionHash
-
end
-
-
1
def cookie_value(data)
-
data.cookie_value
-
end
-
end
-
-
1
class ID < Persisted
-
1
def self.inherited(klass)
-
k = klass.ancestors.find { |kl| kl.respond_to?(:superclass) && kl.superclass == ID }
-
unless k.instance_variable_defined?(:"@_rack_warned")
-
warn "#{klass} is inheriting from #{ID}. Inheriting from #{ID} is deprecated, please inherit from #{Persisted} instead" if $VERBOSE
-
k.instance_variable_set(:"@_rack_warned", true)
-
end
-
super
-
end
-
-
# All thread safety and session retrieval procedures should occur here.
-
# Should return [session_id, session].
-
# If nil is provided as the session id, generation of a new valid id
-
# should occur within.
-
-
1
def find_session(req, sid)
-
get_session req.env, sid
-
end
-
-
# All thread safety and session storage procedures should occur here.
-
# Must return the session id if the session was saved successfully, or
-
# false if the session could not be saved.
-
-
1
def write_session(req, sid, session, options)
-
set_session req.env, sid, session, options
-
end
-
-
# All thread safety and session destroy procedures should occur here.
-
# Should return a new session id or nil if options[:drop]
-
-
1
def delete_session(req, sid, options)
-
destroy_session req.env, sid, options
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require 'openssl'
-
1
require 'zlib'
-
1
require_relative 'abstract/id'
-
1
require 'json'
-
1
require 'base64'
-
-
1
module Rack
-
-
1
module Session
-
-
# Rack::Session::Cookie provides simple cookie based session management.
-
# By default, the session is a Ruby Hash stored as base64 encoded marshalled
-
# data set to :key (default: rack.session). The object that encodes the
-
# session data is configurable and must respond to +encode+ and +decode+.
-
# Both methods must take a string and return a string.
-
#
-
# When the secret key is set, cookie data is checked for data integrity.
-
# The old secret key is also accepted and allows graceful secret rotation.
-
#
-
# Example:
-
#
-
# use Rack::Session::Cookie, :key => 'rack.session',
-
# :domain => 'foo.com',
-
# :path => '/',
-
# :expire_after => 2592000,
-
# :secret => 'change_me',
-
# :old_secret => 'also_change_me'
-
#
-
# All parameters are optional.
-
#
-
# Example of a cookie with no encoding:
-
#
-
# Rack::Session::Cookie.new(application, {
-
# :coder => Rack::Session::Cookie::Identity.new
-
# })
-
#
-
# Example of a cookie with custom encoding:
-
#
-
# Rack::Session::Cookie.new(application, {
-
# :coder => Class.new {
-
# def encode(str); str.reverse; end
-
# def decode(str); str.reverse; end
-
# }.new
-
# })
-
#
-
-
1
class Cookie < Abstract::PersistedSecure
-
# Encode session cookies as Base64
-
1
class Base64
-
1
def encode(str)
-
::Base64.strict_encode64(str)
-
end
-
-
1
def decode(str)
-
::Base64.decode64(str)
-
end
-
-
# Encode session cookies as Marshaled Base64 data
-
1
class Marshal < Base64
-
1
def encode(str)
-
super(::Marshal.dump(str))
-
end
-
-
1
def decode(str)
-
return unless str
-
::Marshal.load(super(str)) rescue nil
-
end
-
end
-
-
# N.B. Unlike other encoding methods, the contained objects must be a
-
# valid JSON composite type, either a Hash or an Array.
-
1
class JSON < Base64
-
1
def encode(obj)
-
super(::JSON.dump(obj))
-
end
-
-
1
def decode(str)
-
return unless str
-
::JSON.parse(super(str)) rescue nil
-
end
-
end
-
-
1
class ZipJSON < Base64
-
1
def encode(obj)
-
super(Zlib::Deflate.deflate(::JSON.dump(obj)))
-
end
-
-
1
def decode(str)
-
return unless str
-
::JSON.parse(Zlib::Inflate.inflate(super(str)))
-
rescue
-
nil
-
end
-
end
-
end
-
-
# Use no encoding for session cookies
-
1
class Identity
-
1
def encode(str); str; end
-
1
def decode(str); str; end
-
end
-
-
1
attr_reader :coder
-
-
1
def initialize(app, options = {})
-
@secrets = options.values_at(:secret, :old_secret).compact
-
@hmac = options.fetch(:hmac, OpenSSL::Digest::SHA1)
-
-
warn <<-MSG unless secure?(options)
-
SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
-
This poses a security threat. It is strongly recommended that you
-
provide a secret to prevent exploits that may be possible from crafted
-
cookies. This will not be supported in future versions of Rack, and
-
future versions will even invalidate your existing user cookies.
-
-
Called from: #{caller[0]}.
-
MSG
-
@coder = options[:coder] ||= Base64::Marshal.new
-
super(app, options.merge!(cookie_only: true))
-
end
-
-
1
private
-
-
1
def find_session(req, sid)
-
data = unpacked_cookie_data(req)
-
data = persistent_session_id!(data)
-
[data["session_id"], data]
-
end
-
-
1
def extract_session_id(request)
-
unpacked_cookie_data(request)["session_id"]
-
end
-
-
1
def unpacked_cookie_data(request)
-
request.fetch_header(RACK_SESSION_UNPACKED_COOKIE_DATA) do |k|
-
session_data = request.cookies[@key]
-
-
if @secrets.size > 0 && session_data
-
session_data, _, digest = session_data.rpartition('--')
-
session_data = nil unless digest_match?(session_data, digest)
-
end
-
-
request.set_header(k, coder.decode(session_data) || {})
-
end
-
end
-
-
1
def persistent_session_id!(data, sid = nil)
-
data ||= {}
-
data["session_id"] ||= sid || generate_sid
-
data
-
end
-
-
1
class SessionId < DelegateClass(Session::SessionId)
-
1
attr_reader :cookie_value
-
-
1
def initialize(session_id, cookie_value)
-
super(session_id)
-
@cookie_value = cookie_value
-
end
-
end
-
-
1
def write_session(req, session_id, session, options)
-
session = session.merge("session_id" => session_id)
-
session_data = coder.encode(session)
-
-
if @secrets.first
-
session_data << "--#{generate_hmac(session_data, @secrets.first)}"
-
end
-
-
if session_data.size > (4096 - @key.size)
-
req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
-
nil
-
else
-
SessionId.new(session_id, session_data)
-
end
-
end
-
-
1
def delete_session(req, session_id, options)
-
# Nothing to do here, data is in the client
-
generate_sid unless options[:drop]
-
end
-
-
1
def digest_match?(data, digest)
-
return unless data && digest
-
@secrets.any? do |secret|
-
Rack::Utils.secure_compare(digest, generate_hmac(data, secret))
-
end
-
end
-
-
1
def generate_hmac(data, secret)
-
OpenSSL::HMAC.hexdigest(@hmac.new, secret, data)
-
end
-
-
1
def secure?(options)
-
@secrets.size >= 1 ||
-
(options[:coder] && options[:let_coder_handle_secure_encoding])
-
end
-
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require 'ostruct'
-
1
require 'erb'
-
-
1
module Rack
-
# Rack::ShowExceptions catches all exceptions raised from the app it
-
# wraps. It shows a useful backtrace with the sourcefile and
-
# clickable context, the whole Rack environment and the request
-
# data.
-
#
-
# Be careful when you use this on public-facing sites as it could
-
# reveal information helpful to attackers.
-
-
1
class ShowExceptions
-
1
CONTEXT = 7
-
-
1
def initialize(app)
-
@app = app
-
end
-
-
1
def call(env)
-
@app.call(env)
-
rescue StandardError, LoadError, SyntaxError => e
-
exception_string = dump_exception(e)
-
-
env[RACK_ERRORS].puts(exception_string)
-
env[RACK_ERRORS].flush
-
-
if accepts_html?(env)
-
content_type = "text/html"
-
body = pretty(env, e)
-
else
-
content_type = "text/plain"
-
body = exception_string
-
end
-
-
[
-
500,
-
{
-
CONTENT_TYPE => content_type,
-
CONTENT_LENGTH => body.bytesize.to_s,
-
},
-
[body],
-
]
-
end
-
-
1
def prefers_plaintext?(env)
-
!accepts_html?(env)
-
end
-
-
1
def accepts_html?(env)
-
Rack::Utils.best_q_match(env["HTTP_ACCEPT"], %w[text/html])
-
end
-
1
private :accepts_html?
-
-
1
def dump_exception(exception)
-
string = "#{exception.class}: #{exception.message}\n".dup
-
string << exception.backtrace.map { |l| "\t#{l}" }.join("\n")
-
string
-
end
-
-
1
def pretty(env, exception)
-
req = Rack::Request.new(env)
-
-
# This double assignment is to prevent an "unused variable" warning.
-
# Yes, it is dumb, but I don't like Ruby yelling at me.
-
path = path = (req.script_name + req.path_info).squeeze("/")
-
-
# This double assignment is to prevent an "unused variable" warning.
-
# Yes, it is dumb, but I don't like Ruby yelling at me.
-
frames = frames = exception.backtrace.map { |line|
-
frame = OpenStruct.new
-
if line =~ /(.*?):(\d+)(:in `(.*)')?/
-
frame.filename = $1
-
frame.lineno = $2.to_i
-
frame.function = $4
-
-
begin
-
lineno = frame.lineno - 1
-
lines = ::File.readlines(frame.filename)
-
frame.pre_context_lineno = [lineno - CONTEXT, 0].max
-
frame.pre_context = lines[frame.pre_context_lineno...lineno]
-
frame.context_line = lines[lineno].chomp
-
frame.post_context_lineno = [lineno + CONTEXT, lines.size].min
-
frame.post_context = lines[lineno + 1..frame.post_context_lineno]
-
rescue
-
end
-
-
frame
-
else
-
nil
-
end
-
}.compact
-
-
template.result(binding)
-
end
-
-
1
def template
-
TEMPLATE
-
end
-
-
1
def h(obj) # :nodoc:
-
case obj
-
when String
-
Utils.escape_html(obj)
-
else
-
Utils.escape_html(obj.inspect)
-
end
-
end
-
-
# :stopdoc:
-
-
# adapted from Django <www.djangoproject.com>
-
# Copyright (c) Django Software Foundation and individual contributors.
-
# Used under the modified BSD license:
-
# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
-
1
TEMPLATE = ERB.new(<<-'HTML'.gsub(/^ /, ''))
-
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
-
<html lang="en">
-
<head>
-
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
-
<meta name="robots" content="NONE,NOARCHIVE" />
-
<title><%=h exception.class %> at <%=h path %></title>
-
<style type="text/css">
-
html * { padding:0; margin:0; }
-
body * { padding:10px 20px; }
-
body * * { padding:0; }
-
body { font:small sans-serif; }
-
body>div { border-bottom:1px solid #ddd; }
-
h1 { font-weight:normal; }
-
h2 { margin-bottom:.8em; }
-
h2 span { font-size:80%; color:#666; font-weight:normal; }
-
h3 { margin:1em 0 .5em 0; }
-
h4 { margin:0 0 .5em 0; font-weight: normal; }
-
table {
-
border:1px solid #ccc; border-collapse: collapse; background:white; }
-
tbody td, tbody th { vertical-align:top; padding:2px 3px; }
-
thead th {
-
padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
-
font-weight:normal; font-size:11px; border:1px solid #ddd; }
-
tbody th { text-align:right; color:#666; padding-right:.5em; }
-
table.vars { margin:5px 0 2px 40px; }
-
table.vars td, table.req td { font-family:monospace; }
-
table td.code { width:100%;}
-
table td.code div { overflow:hidden; }
-
table.source th { color:#666; }
-
table.source td {
-
font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
-
ul.traceback { list-style-type:none; }
-
ul.traceback li.frame { margin-bottom:1em; }
-
div.context { margin: 10px 0; }
-
div.context ol {
-
padding-left:30px; margin:0 10px; list-style-position: inside; }
-
div.context ol li {
-
font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
-
div.context ol.context-line li { color:black; background-color:#ccc; }
-
div.context ol.context-line li span { float: right; }
-
div.commands { margin-left: 40px; }
-
div.commands a { color:black; text-decoration:none; }
-
#summary { background: #ffc; }
-
#summary h2 { font-weight: normal; color: #666; }
-
#summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; }
-
#summary ul#quicklinks li { float: left; padding: 0 1em; }
-
#summary ul#quicklinks>li+li { border-left: 1px #666 solid; }
-
#explanation { background:#eee; }
-
#template, #template-not-exist { background:#f6f6f6; }
-
#template-not-exist ul { margin: 0 0 0 20px; }
-
#traceback { background:#eee; }
-
#requestinfo { background:#f6f6f6; padding-left:120px; }
-
#summary table { border:none; background:transparent; }
-
#requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
-
#requestinfo h3 { margin-bottom:-1em; }
-
.error { background: #ffc; }
-
.specific { color:#cc3300; font-weight:bold; }
-
</style>
-
<script type="text/javascript">
-
//<!--
-
function getElementsByClassName(oElm, strTagName, strClassName){
-
// Written by Jonathan Snook, http://www.snook.ca/jon;
-
// Add-ons by Robert Nyman, http://www.robertnyman.com
-
var arrElements = (strTagName == "*" && document.all)? document.all :
-
oElm.getElementsByTagName(strTagName);
-
var arrReturnElements = new Array();
-
strClassName = strClassName.replace(/\-/g, "\\-");
-
var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$$)");
-
var oElement;
-
for(var i=0; i<arrElements.length; i++){
-
oElement = arrElements[i];
-
if(oRegExp.test(oElement.className)){
-
arrReturnElements.push(oElement);
-
}
-
}
-
return (arrReturnElements)
-
}
-
function hideAll(elems) {
-
for (var e = 0; e < elems.length; e++) {
-
elems[e].style.display = 'none';
-
}
-
}
-
window.onload = function() {
-
hideAll(getElementsByClassName(document, 'table', 'vars'));
-
hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
-
hideAll(getElementsByClassName(document, 'ol', 'post-context'));
-
}
-
function toggle() {
-
for (var i = 0; i < arguments.length; i++) {
-
var e = document.getElementById(arguments[i]);
-
if (e) {
-
e.style.display = e.style.display == 'none' ? 'block' : 'none';
-
}
-
}
-
return false;
-
}
-
function varToggle(link, id) {
-
toggle('v' + id);
-
var s = link.getElementsByTagName('span')[0];
-
var uarr = String.fromCharCode(0x25b6);
-
var darr = String.fromCharCode(0x25bc);
-
s.innerHTML = s.innerHTML == uarr ? darr : uarr;
-
return false;
-
}
-
//-->
-
</script>
-
</head>
-
<body>
-
-
<div id="summary">
-
<h1><%=h exception.class %> at <%=h path %></h1>
-
<h2><%=h exception.message %></h2>
-
<table><tr>
-
<th>Ruby</th>
-
<td>
-
<% if first = frames.first %>
-
<code><%=h first.filename %></code>: in <code><%=h first.function %></code>, line <%=h frames.first.lineno %>
-
<% else %>
-
unknown location
-
<% end %>
-
</td>
-
</tr><tr>
-
<th>Web</th>
-
<td><code><%=h req.request_method %> <%=h(req.host + path)%></code></td>
-
</tr></table>
-
-
<h3>Jump to:</h3>
-
<ul id="quicklinks">
-
<li><a href="#get-info">GET</a></li>
-
<li><a href="#post-info">POST</a></li>
-
<li><a href="#cookie-info">Cookies</a></li>
-
<li><a href="#env-info">ENV</a></li>
-
</ul>
-
</div>
-
-
<div id="traceback">
-
<h2>Traceback <span>(innermost first)</span></h2>
-
<ul class="traceback">
-
<% frames.each { |frame| %>
-
<li class="frame">
-
<code><%=h frame.filename %></code>: in <code><%=h frame.function %></code>
-
-
<% if frame.context_line %>
-
<div class="context" id="c<%=h frame.object_id %>">
-
<% if frame.pre_context %>
-
<ol start="<%=h frame.pre_context_lineno+1 %>" class="pre-context" id="pre<%=h frame.object_id %>">
-
<% frame.pre_context.each { |line| %>
-
<li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
-
<% } %>
-
</ol>
-
<% end %>
-
-
<ol start="<%=h frame.lineno %>" class="context-line">
-
<li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h frame.context_line %><span>...</span></li></ol>
-
-
<% if frame.post_context %>
-
<ol start='<%=h frame.lineno+1 %>' class="post-context" id="post<%=h frame.object_id %>">
-
<% frame.post_context.each { |line| %>
-
<li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
-
<% } %>
-
</ol>
-
<% end %>
-
</div>
-
<% end %>
-
</li>
-
<% } %>
-
</ul>
-
</div>
-
-
<div id="requestinfo">
-
<h2>Request information</h2>
-
-
<h3 id="get-info">GET</h3>
-
<% if req.GET and not req.GET.empty? %>
-
<table class="req">
-
<thead>
-
<tr>
-
<th>Variable</th>
-
<th>Value</th>
-
</tr>
-
</thead>
-
<tbody>
-
<% req.GET.sort_by { |k, v| k.to_s }.each { |key, val| %>
-
<tr>
-
<td><%=h key %></td>
-
<td class="code"><div><%=h val.inspect %></div></td>
-
</tr>
-
<% } %>
-
</tbody>
-
</table>
-
<% else %>
-
<p>No GET data.</p>
-
<% end %>
-
-
<h3 id="post-info">POST</h3>
-
<% if ((req.POST and not req.POST.empty?) rescue (no_post_data = "Invalid POST data"; nil)) %>
-
<table class="req">
-
<thead>
-
<tr>
-
<th>Variable</th>
-
<th>Value</th>
-
</tr>
-
</thead>
-
<tbody>
-
<% req.POST.sort_by { |k, v| k.to_s }.each { |key, val| %>
-
<tr>
-
<td><%=h key %></td>
-
<td class="code"><div><%=h val.inspect %></div></td>
-
</tr>
-
<% } %>
-
</tbody>
-
</table>
-
<% else %>
-
<p><%= no_post_data || "No POST data" %>.</p>
-
<% end %>
-
-
-
<h3 id="cookie-info">COOKIES</h3>
-
<% unless req.cookies.empty? %>
-
<table class="req">
-
<thead>
-
<tr>
-
<th>Variable</th>
-
<th>Value</th>
-
</tr>
-
</thead>
-
<tbody>
-
<% req.cookies.each { |key, val| %>
-
<tr>
-
<td><%=h key %></td>
-
<td class="code"><div><%=h val.inspect %></div></td>
-
</tr>
-
<% } %>
-
</tbody>
-
</table>
-
<% else %>
-
<p>No cookie data.</p>
-
<% end %>
-
-
<h3 id="env-info">Rack ENV</h3>
-
<table class="req">
-
<thead>
-
<tr>
-
<th>Variable</th>
-
<th>Value</th>
-
</tr>
-
</thead>
-
<tbody>
-
<% env.sort_by { |k, v| k.to_s }.each { |key, val| %>
-
<tr>
-
<td><%=h key %></td>
-
<td class="code"><div><%=h val.inspect %></div></td>
-
</tr>
-
<% } %>
-
</tbody>
-
</table>
-
-
</div>
-
-
<div id="explanation">
-
<p>
-
You're seeing this error because you use <code>Rack::ShowExceptions</code>.
-
</p>
-
</div>
-
-
</body>
-
</html>
-
HTML
-
-
# :startdoc:
-
end
-
end
-
# -*- encoding: binary -*-
-
# frozen_string_literal: true
-
-
1
require 'uri'
-
1
require 'fileutils'
-
1
require 'set'
-
1
require 'tempfile'
-
1
require 'time'
-
-
1
require_relative 'query_parser'
-
-
1
module Rack
-
# Rack::Utils contains a grab-bag of useful methods for writing web
-
# applications adopted from all kinds of Ruby libraries.
-
-
1
module Utils
-
1
(require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
-
-
1
ParameterTypeError = QueryParser::ParameterTypeError
-
1
InvalidParameterError = QueryParser::InvalidParameterError
-
1
DEFAULT_SEP = QueryParser::DEFAULT_SEP
-
1
COMMON_SEP = QueryParser::COMMON_SEP
-
1
KeySpaceConstrainedParams = QueryParser::Params
-
-
1
class << self
-
1
attr_accessor :default_query_parser
-
end
-
# The default number of bytes to allow parameter keys to take up.
-
# This helps prevent a rogue client from flooding a Request.
-
1
self.default_query_parser = QueryParser.make_default(65536, 100)
-
-
1
module_function
-
-
# URI escapes. (CGI style space to +)
-
1
def escape(s)
-
URI.encode_www_form_component(s)
-
end
-
-
# Like URI escaping, but with %20 instead of +. Strictly speaking this is
-
# true URI escaping.
-
1
def escape_path(s)
-
::URI::DEFAULT_PARSER.escape s
-
end
-
-
# Unescapes the **path** component of a URI. See Rack::Utils.unescape for
-
# unescaping query parameters or form components.
-
1
def unescape_path(s)
-
::URI::DEFAULT_PARSER.unescape s
-
end
-
-
# Unescapes a URI escaped string with +encoding+. +encoding+ will be the
-
# target encoding of the string returned, and it defaults to UTF-8
-
1
def unescape(s, encoding = Encoding::UTF_8)
-
2
URI.decode_www_form_component(s, encoding)
-
end
-
-
1
class << self
-
1
attr_accessor :multipart_part_limit
-
end
-
-
# The maximum number of parts a request can contain. Accepting too many part
-
# can lead to the server running out of file handles.
-
# Set to `0` for no limit.
-
1
self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i
-
-
1
def self.param_depth_limit
-
default_query_parser.param_depth_limit
-
end
-
-
1
def self.param_depth_limit=(v)
-
self.default_query_parser = self.default_query_parser.new_depth_limit(v)
-
end
-
-
1
def self.key_space_limit
-
default_query_parser.key_space_limit
-
end
-
-
1
def self.key_space_limit=(v)
-
self.default_query_parser = self.default_query_parser.new_space_limit(v)
-
end
-
-
1
if defined?(Process::CLOCK_MONOTONIC)
-
1
def clock_time
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
-
end
-
else
-
skipped
# :nocov:
-
skipped
def clock_time
-
skipped
Time.now.to_f
-
skipped
end
-
skipped
# :nocov:
-
end
-
-
1
def parse_query(qs, d = nil, &unescaper)
-
Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
-
end
-
-
1
def parse_nested_query(qs, d = nil)
-
Rack::Utils.default_query_parser.parse_nested_query(qs, d)
-
end
-
-
1
def build_query(params)
-
params.map { |k, v|
-
if v.class == Array
-
build_query(v.map { |x| [k, x] })
-
else
-
v.nil? ? escape(k) : "#{escape(k)}=#{escape(v)}"
-
end
-
}.join("&")
-
end
-
-
1
def build_nested_query(value, prefix = nil)
-
case value
-
when Array
-
value.map { |v|
-
build_nested_query(v, "#{prefix}[]")
-
}.join("&")
-
when Hash
-
value.map { |k, v|
-
build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
-
}.delete_if(&:empty?).join('&')
-
when nil
-
prefix
-
else
-
raise ArgumentError, "value must be a Hash" if prefix.nil?
-
"#{prefix}=#{escape(value)}"
-
end
-
end
-
-
1
def q_values(q_value_header)
-
q_value_header.to_s.split(/\s*,\s*/).map do |part|
-
value, parameters = part.split(/\s*;\s*/, 2)
-
quality = 1.0
-
if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
-
quality = md[1].to_f
-
end
-
[value, quality]
-
end
-
end
-
-
# Return best accept value to use, based on the algorithm
-
# in RFC 2616 Section 14. If there are multiple best
-
# matches (same specificity and quality), the value returned
-
# is arbitrary.
-
1
def best_q_match(q_value_header, available_mimes)
-
values = q_values(q_value_header)
-
-
matches = values.map do |req_mime, quality|
-
match = available_mimes.find { |am| Rack::Mime.match?(am, req_mime) }
-
next unless match
-
[match, quality]
-
end.compact.sort_by do |match, quality|
-
(match.split('/', 2).count('*') * -10) + quality
-
end.last
-
matches && matches.first
-
end
-
-
1
ESCAPE_HTML = {
-
"&" => "&",
-
"<" => "<",
-
">" => ">",
-
"'" => "'",
-
'"' => """,
-
"/" => "/"
-
}
-
-
1
ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
-
-
# Escape ampersands, brackets and quotes to their HTML/XML entities.
-
1
def escape_html(string)
-
string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
-
end
-
-
1
def select_best_encoding(available_encodings, accept_encoding)
-
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
-
-
expanded_accept_encoding = []
-
-
accept_encoding.each do |m, q|
-
preference = available_encodings.index(m) || available_encodings.size
-
-
if m == "*"
-
(available_encodings - accept_encoding.map(&:first)).each do |m2|
-
expanded_accept_encoding << [m2, q, preference]
-
end
-
else
-
expanded_accept_encoding << [m, q, preference]
-
end
-
end
-
-
encoding_candidates = expanded_accept_encoding
-
.sort_by { |_, q, p| [-q, p] }
-
.map!(&:first)
-
-
unless encoding_candidates.include?("identity")
-
encoding_candidates.push("identity")
-
end
-
-
expanded_accept_encoding.each do |m, q|
-
encoding_candidates.delete(m) if q == 0.0
-
end
-
-
(encoding_candidates & available_encodings)[0]
-
end
-
-
1
def parse_cookies(env)
-
parse_cookies_header env[HTTP_COOKIE]
-
end
-
-
1
def parse_cookies_header(header)
-
# According to RFC 6265:
-
# The syntax for cookie headers only supports semicolons
-
# User Agent -> Server ==
-
# Cookie: SID=31d4d96e407aad42; lang=en-US
-
return {} unless header
-
header.split(/[;] */n).each_with_object({}) do |cookie, cookies|
-
next if cookie.empty?
-
key, value = cookie.split('=', 2)
-
cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
-
end
-
end
-
-
1
def add_cookie_to_header(header, key, value)
-
case value
-
when Hash
-
domain = "; domain=#{value[:domain]}" if value[:domain]
-
path = "; path=#{value[:path]}" if value[:path]
-
max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
-
expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
-
secure = "; secure" if value[:secure]
-
httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
-
same_site =
-
case value[:same_site]
-
when false, nil
-
nil
-
when :none, 'None', :None
-
'; SameSite=None'
-
when :lax, 'Lax', :Lax
-
'; SameSite=Lax'
-
when true, :strict, 'Strict', :Strict
-
'; SameSite=Strict'
-
else
-
raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
-
end
-
value = value[:value]
-
end
-
value = [value] unless Array === value
-
-
cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
-
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
-
-
case header
-
when nil, ''
-
cookie
-
when String
-
[header, cookie].join("\n")
-
when Array
-
(header + [cookie]).join("\n")
-
else
-
raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
-
end
-
end
-
-
1
def set_cookie_header!(header, key, value)
-
header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
-
nil
-
end
-
-
1
def make_delete_cookie_header(header, key, value)
-
case header
-
when nil, ''
-
cookies = []
-
when String
-
cookies = header.split("\n")
-
when Array
-
cookies = header
-
end
-
-
key = escape(key)
-
domain = value[:domain]
-
path = value[:path]
-
regexp = if domain
-
if path
-
/\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
-
else
-
/\A#{key}=.*domain=#{domain}(?:;|$)/
-
end
-
elsif path
-
/\A#{key}=.*path=#{path}(?:;|$)/
-
else
-
/\A#{key}=/
-
end
-
-
cookies.reject! { |cookie| regexp.match? cookie }
-
-
cookies.join("\n")
-
end
-
-
1
def delete_cookie_header!(header, key, value = {})
-
header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
-
nil
-
end
-
-
# Adds a cookie that will *remove* a cookie from the client. Hence the
-
# strange method name.
-
1
def add_remove_cookie_to_header(header, key, value = {})
-
new_header = make_delete_cookie_header(header, key, value)
-
-
add_cookie_to_header(new_header, key,
-
{ value: '', path: nil, domain: nil,
-
max_age: '0',
-
expires: Time.at(0) }.merge(value))
-
-
end
-
-
1
def rfc2822(time)
-
time.rfc2822
-
end
-
-
# Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
-
# of '% %b %Y'.
-
# It assumes that the time is in GMT to comply to the RFC 2109.
-
#
-
# NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
-
# that I'm certain someone implemented only that option.
-
# Do not use %a and %b from Time.strptime, it would use localized names for
-
# weekday and month.
-
#
-
1
def rfc2109(time)
-
wday = Time::RFC2822_DAY_NAME[time.wday]
-
mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
-
time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
-
end
-
-
# Parses the "Range:" header, if present, into an array of Range objects.
-
# Returns nil if the header is missing or syntactically invalid.
-
# Returns an empty array if none of the ranges are satisfiable.
-
1
def byte_ranges(env, size)
-
warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
-
get_byte_ranges env['HTTP_RANGE'], size
-
end
-
-
1
def get_byte_ranges(http_range, size)
-
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
-
return nil unless http_range && http_range =~ /bytes=([^;]+)/
-
ranges = []
-
$1.split(/,\s*/).each do |range_spec|
-
return nil unless range_spec =~ /(\d*)-(\d*)/
-
r0, r1 = $1, $2
-
if r0.empty?
-
return nil if r1.empty?
-
# suffix-byte-range-spec, represents trailing suffix of file
-
r0 = size - r1.to_i
-
r0 = 0 if r0 < 0
-
r1 = size - 1
-
else
-
r0 = r0.to_i
-
if r1.empty?
-
r1 = size - 1
-
else
-
r1 = r1.to_i
-
return nil if r1 < r0 # backwards range is syntactically invalid
-
r1 = size - 1 if r1 >= size
-
end
-
end
-
ranges << (r0..r1) if r0 <= r1
-
end
-
ranges
-
end
-
-
# Constant time string comparison.
-
#
-
# NOTE: the values compared should be of fixed length, such as strings
-
# that have already been processed by HMAC. This should not be used
-
# on variable length plaintext strings because it could leak length info
-
# via timing attacks.
-
1
def secure_compare(a, b)
-
return false unless a.bytesize == b.bytesize
-
-
l = a.unpack("C*")
-
-
r, i = 0, -1
-
b.each_byte { |v| r |= v ^ l[i += 1] }
-
r == 0
-
end
-
-
# Context allows the use of a compatible middleware at different points
-
# in a request handling stack. A compatible middleware must define
-
# #context which should take the arguments env and app. The first of which
-
# would be the request environment. The second of which would be the rack
-
# application that the request would be forwarded to.
-
1
class Context
-
1
attr_reader :for, :app
-
-
1
def initialize(app_f, app_r)
-
raise 'running context does not respond to #context' unless app_f.respond_to? :context
-
@for, @app = app_f, app_r
-
end
-
-
1
def call(env)
-
@for.context(env, @app)
-
end
-
-
1
def recontext(app)
-
self.class.new(@for, app)
-
end
-
-
1
def context(env, app = @app)
-
recontext(app).call(env)
-
end
-
end
-
-
# A case-insensitive Hash that preserves the original case of a
-
# header when set.
-
#
-
# @api private
-
1
class HeaderHash < Hash # :nodoc:
-
1
def self.[](headers)
-
4
if headers.is_a?(HeaderHash) && !headers.frozen?
-
2
return headers
-
else
-
2
return self.new(headers)
-
end
-
end
-
-
1
def initialize(hash = {})
-
2
super()
-
2
@names = {}
-
2
hash.each { |k, v| self[k] = v }
-
end
-
-
# on dup/clone, we need to duplicate @names hash
-
1
def initialize_copy(other)
-
super
-
@names = other.names.dup
-
end
-
-
# on clear, we need to clear @names hash
-
1
def clear
-
super
-
@names.clear
-
end
-
-
1
def each
-
4
super do |k, v|
-
4
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
-
end
-
end
-
-
1
def to_hash
-
hash = {}
-
each { |k, v| hash[k] = v }
-
hash
-
end
-
-
1
def [](k)
-
24
super(k) || super(@names[k.downcase])
-
end
-
-
1
def []=(k, v)
-
22
canonical = k.downcase.freeze
-
22
delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
-
22
@names[canonical] = k
-
22
super k, v
-
end
-
-
1
def delete(k)
-
2
canonical = k.downcase
-
2
result = super @names.delete(canonical)
-
2
result
-
end
-
-
1
def include?(k)
-
2
super || @names.include?(k.downcase)
-
end
-
-
1
alias_method :has_key?, :include?
-
1
alias_method :member?, :include?
-
1
alias_method :key?, :include?
-
-
1
def merge!(other)
-
12
other.each { |k, v| self[k] = v }
-
2
self
-
end
-
-
1
def merge(other)
-
hash = dup
-
hash.merge! other
-
end
-
-
1
def replace(other)
-
clear
-
other.each { |k, v| self[k] = v }
-
self
-
end
-
-
1
protected
-
1
def names
-
@names
-
end
-
end
-
-
# Every standard HTTP code mapped to the appropriate message.
-
# Generated with:
-
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
-
# ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
-
# puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
-
1
HTTP_STATUS_CODES = {
-
100 => 'Continue',
-
101 => 'Switching Protocols',
-
102 => 'Processing',
-
103 => 'Early Hints',
-
200 => 'OK',
-
201 => 'Created',
-
202 => 'Accepted',
-
203 => 'Non-Authoritative Information',
-
204 => 'No Content',
-
205 => 'Reset Content',
-
206 => 'Partial Content',
-
207 => 'Multi-Status',
-
208 => 'Already Reported',
-
226 => 'IM Used',
-
300 => 'Multiple Choices',
-
301 => 'Moved Permanently',
-
302 => 'Found',
-
303 => 'See Other',
-
304 => 'Not Modified',
-
305 => 'Use Proxy',
-
306 => '(Unused)',
-
307 => 'Temporary Redirect',
-
308 => 'Permanent Redirect',
-
400 => 'Bad Request',
-
401 => 'Unauthorized',
-
402 => 'Payment Required',
-
403 => 'Forbidden',
-
404 => 'Not Found',
-
405 => 'Method Not Allowed',
-
406 => 'Not Acceptable',
-
407 => 'Proxy Authentication Required',
-
408 => 'Request Timeout',
-
409 => 'Conflict',
-
410 => 'Gone',
-
411 => 'Length Required',
-
412 => 'Precondition Failed',
-
413 => 'Payload Too Large',
-
414 => 'URI Too Long',
-
415 => 'Unsupported Media Type',
-
416 => 'Range Not Satisfiable',
-
417 => 'Expectation Failed',
-
421 => 'Misdirected Request',
-
422 => 'Unprocessable Entity',
-
423 => 'Locked',
-
424 => 'Failed Dependency',
-
425 => 'Too Early',
-
426 => 'Upgrade Required',
-
428 => 'Precondition Required',
-
429 => 'Too Many Requests',
-
431 => 'Request Header Fields Too Large',
-
451 => 'Unavailable for Legal Reasons',
-
500 => 'Internal Server Error',
-
501 => 'Not Implemented',
-
502 => 'Bad Gateway',
-
503 => 'Service Unavailable',
-
504 => 'Gateway Timeout',
-
505 => 'HTTP Version Not Supported',
-
506 => 'Variant Also Negotiates',
-
507 => 'Insufficient Storage',
-
508 => 'Loop Detected',
-
509 => 'Bandwidth Limit Exceeded',
-
510 => 'Not Extended',
-
511 => 'Network Authentication Required'
-
}
-
-
# Responses with HTTP status codes that should not have an entity body
-
1
STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
-
-
1
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
-
63
[message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
-
}.flatten]
-
-
1
def status_code(status)
-
2
if status.is_a?(Symbol)
-
SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
-
else
-
2
status.to_i
-
end
-
end
-
-
1
PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
-
-
1
def clean_path_info(path_info)
-
parts = path_info.split PATH_SEPS
-
-
clean = []
-
-
parts.each do |part|
-
next if part.empty? || part == '.'
-
part == '..' ? clean.pop : clean << part
-
end
-
-
clean_path = clean.join(::File::SEPARATOR)
-
clean_path.prepend("/") if parts.empty? || parts.first.empty?
-
clean_path
-
end
-
-
1
NULL_BYTE = "\0"
-
-
1
def valid_path?(path)
-
path.valid_encoding? && !path.include?(NULL_BYTE)
-
end
-
-
end
-
end
-
# frozen_string_literal: true
-
-
# Copyright (C) 2007-2019 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
-
#
-
# Rack is freely distributable under the terms of an MIT-style license.
-
# See MIT-LICENSE or https://opensource.org/licenses/MIT.
-
-
# The Rack main module, serving as a namespace for all core Rack
-
# modules and classes.
-
#
-
# All modules meant for use in your application are <tt>autoload</tt>ed here,
-
# so it should be enough just to <tt>require 'rack'</tt> in your code.
-
-
1
module Rack
-
# The Rack protocol version number implemented.
-
1
VERSION = [1, 3]
-
-
# Return the Rack protocol version as a dotted string.
-
1
def self.version
-
VERSION.join(".")
-
end
-
-
1
RELEASE = "2.2.3"
-
-
# Return the Rack release as a dotted string.
-
1
def self.release
-
RELEASE
-
end
-
end
-
1
require 'rack/protection/version'
-
1
require 'rack'
-
-
1
module Rack
-
1
module Protection
-
1
autoload :AuthenticityToken, 'rack/protection/authenticity_token'
-
1
autoload :Base, 'rack/protection/base'
-
1
autoload :CookieTossing, 'rack/protection/cookie_tossing'
-
1
autoload :ContentSecurityPolicy, 'rack/protection/content_security_policy'
-
1
autoload :EscapedParams, 'rack/protection/escaped_params'
-
1
autoload :FormToken, 'rack/protection/form_token'
-
1
autoload :FrameOptions, 'rack/protection/frame_options'
-
1
autoload :HttpOrigin, 'rack/protection/http_origin'
-
1
autoload :IPSpoofing, 'rack/protection/ip_spoofing'
-
1
autoload :JsonCsrf, 'rack/protection/json_csrf'
-
1
autoload :PathTraversal, 'rack/protection/path_traversal'
-
1
autoload :RemoteReferrer, 'rack/protection/remote_referrer'
-
1
autoload :RemoteToken, 'rack/protection/remote_token'
-
1
autoload :SessionHijacking, 'rack/protection/session_hijacking'
-
1
autoload :StrictTransport, 'rack/protection/strict_transport'
-
1
autoload :XSSHeader, 'rack/protection/xss_header'
-
-
1
def self.new(app, options = {})
-
# does not include: RemoteReferrer, AuthenticityToken and FormToken
-
2
except = Array options[:except]
-
2
use_these = Array options[:use]
-
-
2
if options.fetch(:without_session, false)
-
2
except += [:session_hijacking, :remote_token]
-
end
-
-
2
Rack::Builder.new do
-
# Off by default, unless added
-
2
use ::Rack::Protection::AuthenticityToken, options if use_these.include? :authenticity_token
-
2
use ::Rack::Protection::CookieTossing, options if use_these.include? :cookie_tossing
-
2
use ::Rack::Protection::ContentSecurityPolicy, options if use_these.include? :content_security_policy
-
2
use ::Rack::Protection::FormToken, options if use_these.include? :form_token
-
2
use ::Rack::Protection::RemoteReferrer, options if use_these.include? :remote_referrer
-
2
use ::Rack::Protection::StrictTransport, options if use_these.include? :strict_transport
-
-
# On by default, unless skipped
-
2
use ::Rack::Protection::FrameOptions, options unless except.include? :frame_options
-
2
use ::Rack::Protection::HttpOrigin, options unless except.include? :http_origin
-
2
use ::Rack::Protection::IPSpoofing, options unless except.include? :ip_spoofing
-
2
use ::Rack::Protection::JsonCsrf, options unless except.include? :json_csrf
-
2
use ::Rack::Protection::PathTraversal, options unless except.include? :path_traversal
-
2
use ::Rack::Protection::RemoteToken, options unless except.include? :remote_token
-
2
use ::Rack::Protection::SessionHijacking, options unless except.include? :session_hijacking
-
2
use ::Rack::Protection::XSSHeader, options unless except.include? :xss_header
-
2
run app
-
end.to_app
-
end
-
end
-
end
-
1
require 'rack/protection'
-
1
require 'rack/utils'
-
1
require 'digest'
-
1
require 'logger'
-
1
require 'uri'
-
-
1
module Rack
-
1
module Protection
-
1
class Base
-
DEFAULT_OPTIONS = {
-
1
:reaction => :default_reaction, :logging => true,
-
:message => 'Forbidden', :encryptor => Digest::SHA1,
-
:session_key => 'rack.session', :status => 403,
-
:allow_empty_referrer => true,
-
:report_key => "protection.failed",
-
:html_types => %w[text/html application/xhtml text/xml application/xml]
-
}
-
-
1
attr_reader :app, :options
-
-
1
def self.default_options(options)
-
12
define_method(:default_options) { super().merge(options) }
-
end
-
-
1
def self.default_reaction(reaction)
-
2
alias_method(:default_reaction, reaction)
-
end
-
-
1
def default_options
-
12
DEFAULT_OPTIONS
-
end
-
-
1
def initialize(app, options = {})
-
12
@app, @options = app, default_options.merge(options)
-
end
-
-
1
def safe?(env)
-
2
%w[GET HEAD OPTIONS TRACE].include? env['REQUEST_METHOD']
-
end
-
-
1
def accepts?(env)
-
raise NotImplementedError, "#{self.class} implementation pending"
-
end
-
-
1
def call(env)
-
4
unless accepts? env
-
instrument env
-
result = react env
-
end
-
4
result or app.call(env)
-
end
-
-
1
def react(env)
-
result = send(options[:reaction], env)
-
result if Array === result and result.size == 3
-
end
-
-
1
def warn(env, message)
-
return unless options[:logging]
-
l = options[:logger] || env['rack.logger'] || ::Logger.new(env['rack.errors'])
-
l.warn(message)
-
end
-
-
1
def instrument(env)
-
return unless i = options[:instrumenter]
-
env['rack.protection.attack'] = self.class.name.split('::').last.downcase
-
i.instrument('rack.protection', env)
-
end
-
-
1
def deny(env)
-
warn env, "attack prevented by #{self.class}"
-
[options[:status], {'Content-Type' => 'text/plain'}, [options[:message]]]
-
end
-
-
1
def report(env)
-
warn env, "attack reported by #{self.class}"
-
env[options[:report_key]] = true
-
end
-
-
1
def session?(env)
-
env.include? options[:session_key]
-
end
-
-
1
def session(env)
-
return env[options[:session_key]] if session? env
-
fail "you need to set up a session middleware *before* #{self.class}"
-
end
-
-
1
def drop_session(env)
-
session(env).clear if session? env
-
end
-
-
1
def referrer(env)
-
ref = env['HTTP_REFERER'].to_s
-
return if !options[:allow_empty_referrer] and ref.empty?
-
URI.parse(ref).host || Request.new(env).host
-
rescue URI::InvalidURIError
-
end
-
-
1
def origin(env)
-
env['HTTP_ORIGIN'] || env['HTTP_X_ORIGIN']
-
end
-
-
1
def random_string(secure = defined? SecureRandom)
-
secure ? SecureRandom.hex(16) : "%032x" % rand(2**128-1)
-
rescue NotImplementedError
-
random_string false
-
end
-
-
1
def encrypt(value)
-
options[:encryptor].hexdigest value.to_s
-
end
-
-
1
def secure_compare(a, b)
-
Rack::Utils.secure_compare(a.to_s, b.to_s)
-
end
-
-
1
alias default_reaction deny
-
-
1
def html?(headers)
-
8
return false unless header = headers.detect { |k,v| k.downcase == 'content-type' }
-
4
options[:html_types].include? header.last[/^\w+\/\w+/]
-
end
-
end
-
end
-
end
-
1
require 'rack/protection'
-
-
1
module Rack
-
1
module Protection
-
##
-
# Prevented attack:: Clickjacking
-
# Supported browsers:: Internet Explorer 8, Firefox 3.6.9, Opera 10.50,
-
# Safari 4.0, Chrome 4.1.249.1042 and later
-
# More infos:: https://developer.mozilla.org/en/The_X-FRAME-OPTIONS_response_header
-
#
-
# Sets X-Frame-Options header to tell the browser avoid embedding the page
-
# in a frame.
-
#
-
# Options:
-
#
-
# frame_options:: Defines who should be allowed to embed the page in a
-
# frame. Use :deny to forbid any embedding, :sameorigin
-
# to allow embedding from the same origin (default).
-
1
class FrameOptions < Base
-
1
default_options :frame_options => :sameorigin
-
-
1
def frame_options
-
@frame_options ||= begin
-
frame_options = options[:frame_options]
-
frame_options = options[:frame_options].to_s.upcase unless frame_options.respond_to? :to_str
-
frame_options.to_str
-
end
-
end
-
-
1
def call(env)
-
2
status, headers, body = @app.call(env)
-
2
headers['X-Frame-Options'] ||= frame_options if html? headers
-
2
[status, headers, body]
-
end
-
end
-
end
-
end
-
1
require 'rack/protection'
-
-
1
module Rack
-
1
module Protection
-
##
-
# Prevented attack:: CSRF
-
# Supported browsers:: Google Chrome 2, Safari 4 and later
-
# More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery
-
# http://tools.ietf.org/html/draft-abarth-origin
-
#
-
# Does not accept unsafe HTTP requests when value of Origin HTTP request header
-
# does not match default or whitelisted URIs.
-
#
-
# If you want to whitelist a specific domain, you can pass in as the `:origin_whitelist` option:
-
#
-
# use Rack::Protection, origin_whitelist: ["http://localhost:3000", "http://127.0.01:3000"]
-
#
-
# The `:allow_if` option can also be set to a proc to use custom allow/deny logic.
-
1
class HttpOrigin < Base
-
1
DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
-
1
default_reaction :deny
-
1
default_options :allow_if => nil
-
-
1
def base_url(env)
-
request = Rack::Request.new(env)
-
port = ":#{request.port}" unless request.port == DEFAULT_PORTS[request.scheme]
-
"#{request.scheme}://#{request.host}#{port}"
-
end
-
-
1
def accepts?(env)
-
2
return true if safe? env
-
return true unless origin = env['HTTP_ORIGIN']
-
return true if base_url(env) == origin
-
return true if options[:allow_if] && options[:allow_if].call(env)
-
Array(options[:origin_whitelist]).include? origin
-
end
-
-
end
-
end
-
end
-
1
require 'rack/protection'
-
-
1
module Rack
-
1
module Protection
-
##
-
# Prevented attack:: IP spoofing
-
# Supported browsers:: all
-
# More infos:: http://blog.c22.cc/2011/04/22/surveymonkey-ip-spoofing/
-
#
-
# Detect (some) IP spoofing attacks.
-
1
class IPSpoofing < Base
-
1
default_reaction :deny
-
-
1
def accepts?(env)
-
2
return true unless env.include? 'HTTP_X_FORWARDED_FOR'
-
ips = env['HTTP_X_FORWARDED_FOR'].split(/\s*,\s*/)
-
return false if env.include? 'HTTP_CLIENT_IP' and not ips.include? env['HTTP_CLIENT_IP']
-
return false if env.include? 'HTTP_X_REAL_IP' and not ips.include? env['HTTP_X_REAL_IP']
-
true
-
end
-
end
-
end
-
end
-
1
require 'rack/protection'
-
-
1
module Rack
-
1
module Protection
-
##
-
# Prevented attack:: CSRF
-
# Supported browsers:: all
-
# More infos:: http://flask.pocoo.org/docs/0.10/security/#json-security
-
# http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx
-
#
-
# JSON GET APIs are vulnerable to being embedded as JavaScript when the
-
# Array prototype has been patched to track data. Checks the referrer
-
# even on GET requests if the content type is JSON.
-
#
-
# If request includes Origin HTTP header, defers to HttpOrigin to determine
-
# if the request is safe. Please refer to the documentation for more info.
-
#
-
# The `:allow_if` option can be set to a proc to use custom allow/deny logic.
-
1
class JsonCsrf < Base
-
1
default_options :allow_if => nil
-
-
1
alias react deny
-
-
1
def call(env)
-
2
request = Request.new(env)
-
2
status, headers, body = app.call(env)
-
-
2
if has_vector?(request, headers)
-
warn env, "attack prevented by #{self.class}"
-
-
react_and_close(env, body) or [status, headers, body]
-
else
-
2
[status, headers, body]
-
end
-
end
-
-
1
def has_vector?(request, headers)
-
2
return false if request.xhr?
-
2
return false if options[:allow_if] && options[:allow_if].call(request.env)
-
2
return false unless headers['Content-Type'].to_s.split(';', 2).first =~ /^\s*application\/json\s*$/
-
origin(request.env).nil? and referrer(request.env) != request.host
-
end
-
-
1
def react_and_close(env, body)
-
reaction = react(env)
-
-
close_body(body) if reaction
-
-
reaction
-
end
-
-
1
def close_body(body)
-
body.close if body.respond_to?(:close)
-
end
-
end
-
end
-
end
-
1
require 'rack/protection'
-
-
1
module Rack
-
1
module Protection
-
##
-
# Prevented attack:: Directory traversal
-
# Supported browsers:: all
-
# More infos:: http://en.wikipedia.org/wiki/Directory_traversal
-
#
-
# Unescapes '/' and '.', expands +path_info+.
-
# Thus <tt>GET /foo/%2e%2e%2fbar</tt> becomes <tt>GET /bar</tt>.
-
1
class PathTraversal < Base
-
1
def call(env)
-
2
path_was = env["PATH_INFO"]
-
2
env["PATH_INFO"] = cleanup path_was if path_was && !path_was.empty?
-
2
app.call env
-
ensure
-
2
env["PATH_INFO"] = path_was
-
end
-
-
1
def cleanup(path)
-
2
encoding = path.encoding
-
2
dot = '.'.encode(encoding)
-
2
slash = '/'.encode(encoding)
-
2
backslash = '\\'.encode(encoding)
-
-
2
parts = []
-
2
unescaped = path.gsub(/%2e/i, dot).gsub(/%2f/i, slash).gsub(/%5c/i, backslash)
-
2
unescaped = unescaped.gsub(backslash, slash)
-
-
2
unescaped.split(slash).each do |part|
-
6
next if part.empty? or part == dot
-
4
part == '..' ? parts.pop : parts << part
-
end
-
-
2
cleaned = slash + parts.join(slash)
-
2
cleaned << slash if parts.any? and unescaped =~ %r{/\.{0,2}$}
-
2
cleaned
-
end
-
end
-
end
-
end
-
1
module Rack
-
1
module Protection
-
1
VERSION = '2.0.8.1'
-
end
-
end
-
1
require 'rack/protection'
-
-
1
module Rack
-
1
module Protection
-
##
-
# Prevented attack:: Non-permanent XSS
-
# Supported browsers:: Internet Explorer 8+ and Chrome
-
# More infos:: http://blogs.msdn.com/b/ie/archive/2008/07/01/ie8-security-part-iv-the-xss-filter.aspx
-
#
-
# Sets X-XSS-Protection header to tell the browser to block attacks.
-
#
-
# Options:
-
# xss_mode:: How the browser should prevent the attack (default: :block)
-
1
class XSSHeader < Base
-
1
default_options :xss_mode => :block, :nosniff => true
-
-
1
def call(env)
-
2
status, headers, body = @app.call(env)
-
2
headers['X-XSS-Protection'] ||= "1; mode=#{options[:xss_mode]}" if html? headers
-
2
headers['X-Content-Type-Options'] ||= 'nosniff' if options[:nosniff]
-
2
[status, headers, body]
-
end
-
end
-
end
-
end
-
1
module Rack
-
1
class MockSession # :nodoc:
-
1
attr_writer :cookie_jar
-
1
attr_reader :default_host
-
-
1
def initialize(app, default_host = Rack::Test::DEFAULT_HOST)
-
2
@app = app
-
2
@after_request = []
-
2
@default_host = default_host
-
2
@last_request = nil
-
2
@last_response = nil
-
end
-
-
1
def after_request(&block)
-
@after_request << block
-
end
-
-
1
def clear_cookies
-
@cookie_jar = Rack::Test::CookieJar.new([], @default_host)
-
end
-
-
1
def set_cookie(cookie, uri = nil)
-
cookie_jar.merge(cookie, uri)
-
end
-
-
1
def request(uri, env)
-
2
env['HTTP_COOKIE'] ||= cookie_jar.for(uri)
-
2
@last_request = Rack::Request.new(env)
-
2
status, headers, body = @app.call(@last_request.env)
-
-
2
@last_response = MockResponse.new(status, headers, body, env['rack.errors'].flush)
-
2
body.close if body.respond_to?(:close)
-
-
2
cookie_jar.merge(last_response.headers['Set-Cookie'], uri)
-
-
2
@after_request.each(&:call)
-
-
2
if @last_response.respond_to?(:finish)
-
2
@last_response.finish
-
else
-
@last_response
-
end
-
end
-
-
# Return the last request issued in the session. Raises an error if no
-
# requests have been sent yet.
-
1
def last_request
-
raise Rack::Test::Error, 'No request yet. Request a page first.' unless @last_request
-
@last_request
-
end
-
-
# Return the last response received in the session. Raises an error if
-
# no requests have been sent yet.
-
1
def last_response
-
14
raise Rack::Test::Error, 'No response yet. Request a page first.' unless @last_response
-
14
@last_response
-
end
-
-
1
def cookie_jar
-
4
@cookie_jar ||= Rack::Test::CookieJar.new([], @default_host)
-
end
-
end
-
end
-
1
require 'uri'
-
1
require 'rack'
-
1
require 'rack/mock_session'
-
1
require 'rack/test/cookie_jar'
-
1
require 'rack/test/mock_digest_request'
-
1
require 'rack/test/utils'
-
1
require 'rack/test/methods'
-
1
require 'rack/test/uploaded_file'
-
1
require 'rack/test/version'
-
-
1
module Rack
-
1
module Test
-
1
DEFAULT_HOST = 'example.org'.freeze
-
1
MULTIPART_BOUNDARY = '----------XnJLe9ZIbbGUYtzPQJ16u1'.freeze
-
-
# The common base class for exceptions raised by Rack::Test
-
1
class Error < StandardError; end
-
-
# This class represents a series of requests issued to a Rack app, sharing
-
# a single cookie jar
-
#
-
# Rack::Test::Session's methods are most often called through Rack::Test::Methods,
-
# which will automatically build a session when it's first used.
-
1
class Session
-
1
extend Forwardable
-
1
include Rack::Test::Utils
-
-
1
def_delegators :@rack_mock_session, :clear_cookies, :set_cookie, :last_response, :last_request
-
-
# Creates a Rack::Test::Session for a given Rack app or Rack::MockSession.
-
#
-
# Note: Generally, you won't need to initialize a Rack::Test::Session directly.
-
# Instead, you should include Rack::Test::Methods into your testing context.
-
# (See README.rdoc for an example)
-
1
def initialize(mock_session)
-
2
@headers = {}
-
2
@env = {}
-
2
@digest_username = nil
-
2
@digest_password = nil
-
-
2
@rack_mock_session = if mock_session.is_a?(MockSession)
-
2
mock_session
-
else
-
MockSession.new(mock_session)
-
end
-
-
2
@default_host = @rack_mock_session.default_host
-
end
-
-
# Issue a GET request for the given URI with the given params and Rack
-
# environment. Stores the issues request object in #last_request and
-
# the app's response in #last_response. Yield #last_response to a block
-
# if given.
-
#
-
# Example:
-
# get "/"
-
1
def get(uri, params = {}, env = {}, &block)
-
2
custom_request('GET', uri, params, env, &block)
-
end
-
-
# Issue a POST request for the given URI. See #get
-
#
-
# Example:
-
# post "/signup", "name" => "Bryan"
-
1
def post(uri, params = {}, env = {}, &block)
-
custom_request('POST', uri, params, env, &block)
-
end
-
-
# Issue a PUT request for the given URI. See #get
-
#
-
# Example:
-
# put "/"
-
1
def put(uri, params = {}, env = {}, &block)
-
custom_request('PUT', uri, params, env, &block)
-
end
-
-
# Issue a PATCH request for the given URI. See #get
-
#
-
# Example:
-
# patch "/"
-
1
def patch(uri, params = {}, env = {}, &block)
-
custom_request('PATCH', uri, params, env, &block)
-
end
-
-
# Issue a DELETE request for the given URI. See #get
-
#
-
# Example:
-
# delete "/"
-
1
def delete(uri, params = {}, env = {}, &block)
-
custom_request('DELETE', uri, params, env, &block)
-
end
-
-
# Issue an OPTIONS request for the given URI. See #get
-
#
-
# Example:
-
# options "/"
-
1
def options(uri, params = {}, env = {}, &block)
-
custom_request('OPTIONS', uri, params, env, &block)
-
end
-
-
# Issue a HEAD request for the given URI. See #get
-
#
-
# Example:
-
# head "/"
-
1
def head(uri, params = {}, env = {}, &block)
-
custom_request('HEAD', uri, params, env, &block)
-
end
-
-
# Issue a request to the Rack app for the given URI and optional Rack
-
# environment. Stores the issues request object in #last_request and
-
# the app's response in #last_response. Yield #last_response to a block
-
# if given.
-
#
-
# Example:
-
# request "/"
-
1
def request(uri, env = {}, &block)
-
uri = parse_uri(uri, env)
-
env = env_for(uri, env)
-
process_request(uri, env, &block)
-
end
-
-
# Issue a request using the given verb for the given URI. See #get
-
#
-
# Example:
-
# custom_request "LINK", "/"
-
1
def custom_request(verb, uri, params = {}, env = {}, &block)
-
2
uri = parse_uri(uri, env)
-
2
env = env_for(uri, env.merge(method: verb.to_s.upcase, params: params))
-
2
process_request(uri, env, &block)
-
end
-
-
# Set a header to be included on all subsequent requests through the
-
# session. Use a value of nil to remove a previously configured header.
-
#
-
# In accordance with the Rack spec, headers will be included in the Rack
-
# environment hash in HTTP_USER_AGENT form.
-
#
-
# Example:
-
# header "User-Agent", "Firefox"
-
1
def header(name, value)
-
if value.nil?
-
@headers.delete(name)
-
else
-
@headers[name] = value
-
end
-
end
-
-
# Set an env var to be included on all subsequent requests through the
-
# session. Use a value of nil to remove a previously configured env.
-
#
-
# Example:
-
# env "rack.session", {:csrf => 'token'}
-
1
def env(name, value)
-
if value.nil?
-
@env.delete(name)
-
else
-
@env[name] = value
-
end
-
end
-
-
# Set the username and password for HTTP Basic authorization, to be
-
# included in subsequent requests in the HTTP_AUTHORIZATION header.
-
#
-
# Example:
-
# basic_authorize "bryan", "secret"
-
1
def basic_authorize(username, password)
-
encoded_login = ["#{username}:#{password}"].pack('m0')
-
header('Authorization', "Basic #{encoded_login}")
-
end
-
-
1
alias authorize basic_authorize
-
-
# Set the username and password for HTTP Digest authorization, to be
-
# included in subsequent requests in the HTTP_AUTHORIZATION header.
-
#
-
# Example:
-
# digest_authorize "bryan", "secret"
-
1
def digest_authorize(username, password)
-
@digest_username = username
-
@digest_password = password
-
end
-
-
# Rack::Test will not follow any redirects automatically. This method
-
# will follow the redirect returned (including setting the Referer header
-
# on the new request) in the last response. If the last response was not
-
# a redirect, an error will be raised.
-
1
def follow_redirect!
-
unless last_response.redirect?
-
raise Error, 'Last response was not a redirect. Cannot follow_redirect!'
-
end
-
request_method, params =
-
if last_response.status == 307
-
[last_request.request_method.downcase.to_sym, last_request.params]
-
else
-
[:get, {}]
-
end
-
-
# Compute the next location by appending the location header with the
-
# last request, as per https://tools.ietf.org/html/rfc7231#section-7.1.2
-
# Adding two absolute locations returns the right-hand location
-
next_location = URI.parse(last_request.url) + URI.parse(last_response['Location'])
-
-
send(
-
request_method, next_location.to_s, params,
-
'HTTP_REFERER' => last_request.url,
-
'rack.session' => last_request.session,
-
'rack.session.options' => last_request.session_options
-
)
-
end
-
-
1
private
-
-
1
def parse_uri(path, env)
-
2
URI.parse(path).tap do |uri|
-
2
uri.path = "/#{uri.path}" unless uri.path[0] == '/'
-
2
uri.host ||= @default_host
-
2
uri.scheme ||= 'https' if env['HTTPS'] == 'on'
-
end
-
end
-
-
1
def env_for(uri, env)
-
2
env = default_env.merge(env)
-
-
2
env['HTTP_HOST'] ||= [uri.host, (uri.port if uri.port != uri.default_port)].compact.join(':')
-
-
2
env.update('HTTPS' => 'on') if URI::HTTPS === uri
-
2
env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' if env[:xhr]
-
-
# TODO: Remove this after Rack 1.1 has been released.
-
# Stringifying and upcasing methods has be commit upstream
-
2
env['REQUEST_METHOD'] ||= env[:method] ? env[:method].to_s.upcase : 'GET'
-
-
2
params = env.delete(:params) do {} end
-
-
2
if env['REQUEST_METHOD'] == 'GET'
-
# merge :params with the query string
-
2
if params
-
2
params = parse_nested_query(params) if params.is_a?(String)
-
-
4
uri.query = [uri.query, build_nested_query(params)].compact.reject { |v| v == '' }.join('&')
-
end
-
elsif !env.key?(:input)
-
env['CONTENT_TYPE'] ||= 'application/x-www-form-urlencoded'
-
-
if params.is_a?(Hash)
-
if data = build_multipart(params)
-
env[:input] = data
-
env['CONTENT_LENGTH'] ||= data.length.to_s
-
env['CONTENT_TYPE'] = "multipart/form-data; boundary=#{MULTIPART_BOUNDARY}"
-
else
-
# NB: We do not need to set CONTENT_LENGTH here;
-
# Rack::ContentLength will determine it automatically.
-
env[:input] = params_to_string(params)
-
end
-
else
-
env[:input] = params
-
end
-
end
-
-
2
set_cookie(env.delete(:cookie), uri) if env.key?(:cookie)
-
-
2
Rack::MockRequest.env_for(uri.to_s, env)
-
end
-
-
1
def process_request(uri, env)
-
2
@rack_mock_session.request(uri, env)
-
-
2
if retry_with_digest_auth?(env)
-
auth_env = env.merge('HTTP_AUTHORIZATION' => digest_auth_header,
-
'rack-test.digest_auth_retry' => true)
-
auth_env.delete('rack.request')
-
process_request(uri.path, auth_env)
-
else
-
2
yield last_response if block_given?
-
-
2
last_response
-
end
-
end
-
-
1
def digest_auth_header
-
challenge = last_response['WWW-Authenticate'].split(' ', 2).last
-
params = Rack::Auth::Digest::Params.parse(challenge)
-
-
params.merge!('username' => @digest_username,
-
'nc' => '00000001',
-
'cnonce' => 'nonsensenonce',
-
'uri' => last_request.fullpath,
-
'method' => last_request.env['REQUEST_METHOD'])
-
-
params['response'] = MockDigestRequest.new(params).response(@digest_password)
-
-
"Digest #{params}"
-
end
-
-
1
def retry_with_digest_auth?(env)
-
2
last_response.status == 401 &&
-
digest_auth_configured? &&
-
!env['rack-test.digest_auth_retry']
-
end
-
-
1
def digest_auth_configured?
-
@digest_username
-
end
-
-
1
def default_env
-
2
{ 'rack.test' => true, 'REMOTE_ADDR' => '127.0.0.1' }.merge(@env).merge(headers_for_env)
-
end
-
-
1
def headers_for_env
-
2
converted_headers = {}
-
-
2
@headers.each do |name, value|
-
env_key = name.upcase.tr('-', '_')
-
env_key = 'HTTP_' + env_key unless env_key == 'CONTENT_TYPE'
-
converted_headers[env_key] = value
-
end
-
-
2
converted_headers
-
end
-
-
1
def params_to_string(params)
-
case params
-
when Hash then build_nested_query(params)
-
when nil then ''
-
else params
-
end
-
end
-
end
-
-
1
def self.encoding_aware_strings?
-
defined?(Encoding) && ''.respond_to?(:encode)
-
end
-
end
-
end
-
1
require 'uri'
-
1
require 'time'
-
-
1
module Rack
-
1
module Test
-
1
class Cookie # :nodoc:
-
1
include Rack::Utils
-
-
# :api: private
-
1
attr_reader :name, :value
-
-
# :api: private
-
1
def initialize(raw, uri = nil, default_host = DEFAULT_HOST)
-
@default_host = default_host
-
uri ||= default_uri
-
-
# separate the name / value pair from the cookie options
-
@name_value_raw, options = raw.split(/[;,] */n, 2)
-
-
@name, @value = parse_query(@name_value_raw, ';').to_a.first
-
@options = parse_query(options, ';')
-
-
@options['domain'] ||= (uri.host || default_host)
-
@options['path'] ||= uri.path.sub(/\/[^\/]*\Z/, '')
-
end
-
-
1
def replaces?(other)
-
[name.downcase, domain, path] == [other.name.downcase, other.domain, other.path]
-
end
-
-
# :api: private
-
1
def raw
-
@name_value_raw
-
end
-
-
# :api: private
-
1
def empty?
-
@value.nil? || @value.empty?
-
end
-
-
# :api: private
-
1
def domain
-
@options['domain']
-
end
-
-
1
def secure?
-
@options.key?('secure')
-
end
-
-
1
def http_only?
-
@options.key?('HttpOnly')
-
end
-
-
# :api: private
-
1
def path
-
([*@options['path']].first.split(',').first || '/').strip
-
end
-
-
# :api: private
-
1
def expires
-
Time.parse(@options['expires']) if @options['expires']
-
end
-
-
# :api: private
-
1
def expired?
-
expires && expires < Time.now
-
end
-
-
# :api: private
-
1
def valid?(uri)
-
uri ||= default_uri
-
-
uri.host = @default_host if uri.host.nil?
-
-
real_domain = domain =~ /^\./ ? domain[1..-1] : domain
-
(!secure? || (secure? && uri.scheme == 'https')) &&
-
uri.host =~ Regexp.new("#{Regexp.escape(real_domain)}$", Regexp::IGNORECASE) &&
-
uri.path =~ Regexp.new("^#{Regexp.escape(path)}")
-
end
-
-
# :api: private
-
1
def matches?(uri)
-
!expired? && valid?(uri)
-
end
-
-
# :api: private
-
1
def <=>(other)
-
# Orders the cookies from least specific to most
-
[name, path, domain.reverse] <=> [other.name, other.path, other.domain.reverse]
-
end
-
-
1
def to_h
-
@options.merge(
-
'value' => @value,
-
'HttpOnly' => http_only?,
-
'secure' => secure?
-
)
-
end
-
1
alias to_hash to_h
-
-
1
protected
-
-
1
def default_uri
-
URI.parse('//' + @default_host + '/')
-
end
-
end
-
-
1
class CookieJar # :nodoc:
-
1
DELIMITER = '; '.freeze
-
-
# :api: private
-
1
def initialize(cookies = [], default_host = DEFAULT_HOST)
-
2
@default_host = default_host
-
2
@cookies = cookies
-
2
@cookies.sort!
-
end
-
-
1
def [](name)
-
cookies = hash_for(nil)
-
# TODO: Should be case insensitive
-
cookies[name.to_s] && cookies[name.to_s].value
-
end
-
-
1
def []=(name, value)
-
merge("#{name}=#{Rack::Utils.escape(value)}")
-
end
-
-
1
def get_cookie(name)
-
hash_for(nil).fetch(name, nil)
-
end
-
-
1
def delete(name)
-
@cookies.reject! do |cookie|
-
cookie.name == name
-
end
-
end
-
-
1
def merge(raw_cookies, uri = nil)
-
2
return unless raw_cookies
-
-
if raw_cookies.is_a? String
-
raw_cookies = raw_cookies.split("\n")
-
raw_cookies.reject!(&:empty?)
-
end
-
-
raw_cookies.each do |raw_cookie|
-
cookie = Cookie.new(raw_cookie, uri, @default_host)
-
self << cookie if cookie.valid?(uri)
-
end
-
end
-
-
1
def <<(new_cookie)
-
@cookies.reject! do |existing_cookie|
-
new_cookie.replaces?(existing_cookie)
-
end
-
-
@cookies << new_cookie
-
@cookies.sort!
-
end
-
-
# :api: private
-
1
def for(uri)
-
2
hash_for(uri).values.map(&:raw).join(DELIMITER)
-
end
-
-
1
def to_hash
-
cookies = {}
-
-
hash_for(nil).each do |name, cookie|
-
cookies[name] = cookie.value
-
end
-
-
cookies
-
end
-
-
1
protected
-
-
1
def hash_for(uri = nil)
-
2
cookies = {}
-
-
# The cookies are sorted by most specific first. So, we loop through
-
# all the cookies in order and add it to a hash by cookie name if
-
# the cookie can be sent to the current URI. It's added to the hash
-
# so that when we are done, the cookies will be unique by name and
-
# we'll have grabbed the most specific to the URI.
-
2
@cookies.each do |cookie|
-
cookies[cookie.name] = cookie if !uri || cookie.matches?(uri)
-
end
-
-
2
cookies
-
end
-
end
-
end
-
end
-
1
require 'forwardable'
-
-
1
module Rack
-
1
module Test
-
# This module serves as the primary integration point for using Rack::Test
-
# in a testing environment. It depends on an app method being defined in the
-
# same context, and provides the Rack::Test API methods (see Rack::Test::Session
-
# for their documentation).
-
#
-
# Example:
-
#
-
# class HomepageTest < Test::Unit::TestCase
-
# include Rack::Test::Methods
-
#
-
# def app
-
# MyApp.new
-
# end
-
# end
-
1
module Methods
-
1
extend Forwardable
-
-
1
def rack_mock_session(name = :default) # :nodoc:
-
2
return build_rack_mock_session unless name
-
-
2
@_rack_mock_sessions ||= {}
-
2
@_rack_mock_sessions[name] ||= build_rack_mock_session
-
end
-
-
1
def build_rack_mock_session # :nodoc:
-
2
Rack::MockSession.new(app)
-
end
-
-
1
def rack_test_session(name = :default) # :nodoc:
-
10
return build_rack_test_session(name) unless name
-
-
10
@_rack_test_sessions ||= {}
-
10
@_rack_test_sessions[name] ||= build_rack_test_session(name)
-
end
-
-
1
def build_rack_test_session(name) # :nodoc:
-
2
Rack::Test::Session.new(rack_mock_session(name))
-
end
-
-
1
def current_session # :nodoc:
-
10
rack_test_session(_current_session_names.last)
-
end
-
-
1
def with_session(name) # :nodoc:
-
_current_session_names.push(name)
-
yield rack_test_session(name)
-
_current_session_names.pop
-
end
-
-
1
def _current_session_names # :nodoc:
-
10
@_current_session_names ||= [:default]
-
end
-
-
1
METHODS = %i[
-
request
-
get
-
post
-
put
-
patch
-
delete
-
options
-
head
-
custom_request
-
follow_redirect!
-
header
-
env
-
set_cookie
-
clear_cookies
-
authorize
-
basic_authorize
-
digest_authorize
-
last_response
-
last_request
-
].freeze
-
-
1
def_delegators :current_session, *METHODS
-
end
-
end
-
end
-
1
module Rack
-
1
module Test
-
1
class MockDigestRequest # :nodoc:
-
1
def initialize(params)
-
@params = params
-
end
-
-
1
def method_missing(sym)
-
if @params.key? k = sym.to_s
-
return @params[k]
-
end
-
-
super
-
end
-
-
1
def method
-
@params['method']
-
end
-
-
1
def response(password)
-
Rack::Auth::Digest::MD5.new(nil).send :digest, self, password
-
end
-
end
-
end
-
end
-
1
require 'fileutils'
-
1
require 'pathname'
-
1
require 'tempfile'
-
-
1
module Rack
-
1
module Test
-
# Wraps a Tempfile with a content type. Including one or more UploadedFile's
-
# in the params causes Rack::Test to build and issue a multipart request.
-
#
-
# Example:
-
# post "/photos", "file" => Rack::Test::UploadedFile.new("me.jpg", "image/jpeg")
-
1
class UploadedFile
-
# The filename, *not* including the path, of the "uploaded" file
-
1
attr_reader :original_filename
-
-
# The tempfile
-
1
attr_reader :tempfile
-
-
# The content type of the "uploaded" file
-
1
attr_accessor :content_type
-
-
# Creates a new UploadedFile instance.
-
#
-
# @param content [IO, Pathname, String, StringIO] a path to a file, or an {IO} or {StringIO} object representing the
-
# file.
-
# @param content_type [String]
-
# @param binary [Boolean] an optional flag that indicates whether the file should be open in binary mode or not.
-
# @param original_filename [String] an optional parameter that provides the original filename if `content` is a StringIO
-
# object. Not used for other kind of `content` objects.
-
1
def initialize(content, content_type = 'text/plain', binary = false, original_filename: nil)
-
if original_filename
-
initialize_from_stringio(content, original_filename)
-
else
-
initialize_from_file_path(content)
-
end
-
@content_type = content_type
-
@tempfile.binmode if binary
-
end
-
-
1
def path
-
tempfile.path
-
end
-
-
1
alias local_path path
-
-
1
def method_missing(method_name, *args, &block) #:nodoc:
-
tempfile.public_send(method_name, *args, &block)
-
end
-
-
1
def respond_to_missing?(method_name, include_private = false) #:nodoc:
-
tempfile.respond_to?(method_name, include_private) || super
-
end
-
-
1
def self.finalize(file)
-
proc { actually_finalize file }
-
end
-
-
1
def self.actually_finalize(file)
-
file.close
-
file.unlink
-
end
-
-
1
private
-
-
1
def initialize_from_stringio(stringio, original_filename)
-
@tempfile = stringio
-
@original_filename = original_filename || raise(ArgumentError, 'Missing `original_filename` for StringIO object')
-
end
-
-
1
def initialize_from_file_path(path)
-
raise "#{path} file does not exist" unless ::File.exist?(path)
-
-
@original_filename = ::File.basename(path)
-
extension = ::File.extname(@original_filename)
-
-
@tempfile = Tempfile.new([::File.basename(@original_filename, extension), extension])
-
@tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
-
-
ObjectSpace.define_finalizer(self, self.class.finalize(@tempfile))
-
-
FileUtils.copy_file(path, @tempfile.path)
-
end
-
end
-
end
-
end
-
1
module Rack
-
1
module Test
-
1
module Utils # :nodoc:
-
1
include Rack::Utils
-
1
extend Rack::Utils
-
-
1
def build_nested_query(value, prefix = nil)
-
2
case value
-
when Array
-
if value.empty?
-
"#{prefix}[]="
-
else
-
value.map do |v|
-
prefix = "#{prefix}[]" unless unescape(prefix) =~ /\[\]$/
-
build_nested_query(v, prefix.to_s)
-
end.join('&')
-
end
-
when Hash
-
2
value.map do |k, v|
-
build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
-
end.join('&')
-
when NilClass
-
prefix.to_s
-
else
-
"#{prefix}=#{escape(value)}"
-
end
-
end
-
1
module_function :build_nested_query
-
-
1
def build_multipart(params, first = true, multipart = false)
-
if first
-
raise ArgumentError, 'value must be a Hash' unless params.is_a?(Hash)
-
-
query = lambda { |value|
-
case value
-
when Array
-
value.each(&query)
-
when Hash
-
value.values.each(&query)
-
when UploadedFile
-
multipart = true
-
end
-
}
-
params.values.each(&query)
-
return nil unless multipart
-
end
-
-
flattened_params = {}
-
-
params.each do |key, value|
-
k = first ? key.to_s : "[#{key}]"
-
-
case value
-
when Array
-
value.map do |v|
-
if v.is_a?(Hash)
-
nested_params = {}
-
build_multipart(v, false).each do |subkey, subvalue|
-
nested_params[subkey] = subvalue
-
end
-
flattened_params["#{k}[]"] ||= []
-
flattened_params["#{k}[]"] << nested_params
-
else
-
flattened_params["#{k}[]"] = value
-
end
-
end
-
when Hash
-
build_multipart(value, false).each do |subkey, subvalue|
-
flattened_params[k + subkey] = subvalue
-
end
-
else
-
flattened_params[k] = value
-
end
-
end
-
-
if first
-
build_parts(flattened_params)
-
else
-
flattened_params
-
end
-
end
-
1
module_function :build_multipart
-
-
1
private
-
-
1
def build_parts(parameters)
-
get_parts(parameters).join + "--#{MULTIPART_BOUNDARY}--\r"
-
end
-
1
module_function :build_parts
-
-
1
def get_parts(parameters)
-
parameters.map do |name, value|
-
if name =~ /\[\]\Z/ && value.is_a?(Array) && value.all? { |v| v.is_a?(Hash) }
-
value.map do |hash|
-
new_value = {}
-
hash.each { |k, v| new_value[name + k] = v }
-
get_parts(new_value).join
-
end.join
-
else
-
if value.respond_to?(:original_filename)
-
build_file_part(name, value)
-
-
elsif value.is_a?(Array) && value.all? { |v| v.respond_to?(:original_filename) }
-
value.map do |v|
-
build_file_part(name, v)
-
end.join
-
-
else
-
primitive_part = build_primitive_part(name, value)
-
Rack::Test.encoding_aware_strings? ? primitive_part.force_encoding('BINARY') : primitive_part
-
end
-
end
-
end
-
end
-
1
module_function :get_parts
-
-
1
def build_primitive_part(parameter_name, value)
-
value = [value] unless value.is_a? Array
-
value.map do |v|
-
<<-EOF
-
--#{MULTIPART_BOUNDARY}\r
-
Content-Disposition: form-data; name="#{parameter_name}"\r
-
\r
-
#{v}\r
-
EOF
-
end.join
-
end
-
1
module_function :build_primitive_part
-
-
1
def build_file_part(parameter_name, uploaded_file)
-
uploaded_file.set_encoding(Encoding::BINARY) if uploaded_file.respond_to?(:set_encoding)
-
<<-EOF
-
--#{MULTIPART_BOUNDARY}\r
-
Content-Disposition: form-data; name="#{parameter_name}"; filename="#{escape(uploaded_file.original_filename)}"\r
-
Content-Type: #{uploaded_file.content_type}\r
-
Content-Length: #{uploaded_file.size}\r
-
\r
-
#{uploaded_file.read}\r
-
EOF
-
end
-
1
module_function :build_file_part
-
end
-
end
-
end
-
1
module Rack
-
1
module Test
-
1
VERSION = '1.1.0'.freeze
-
end
-
end
-
1
class Module
-
1
unless respond_to?(:ruby2_keywords, true)
-
private
-
def ruby2_keywords(name, *)
-
# nil
-
end
-
end
-
end
-
-
1
main = TOPLEVEL_BINDING.receiver
-
1
unless main.respond_to?(:ruby2_keywords, true)
-
def main.ruby2_keywords(name, *)
-
# nil
-
end
-
end
-
-
1
class Proc
-
1
unless method_defined?(:ruby2_keywords)
-
def ruby2_keywords
-
self
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
# The global load paths for Sass files. This is meant for plugins and
-
# libraries to register the paths to their Sass stylesheets to that they may
-
# be `@imported`. This load path is used by every instance of {Sass::Engine}.
-
# They are lower-precedence than any load paths passed in via the
-
# {file:SASS_REFERENCE.md#load_paths-option `:load_paths` option}.
-
#
-
# If the `SASS_PATH` environment variable is set,
-
# the initial value of `load_paths` will be initialized based on that.
-
# The variable should be a colon-separated list of path names
-
# (semicolon-separated on Windows).
-
#
-
# Note that files on the global load path are never compiled to CSS
-
# themselves, even if they aren't partials. They exist only to be imported.
-
#
-
# @example
-
# SassC.load_paths << File.dirname(__FILE__ + '/sass')
-
# @return [Array<String, Pathname, Sass::Importers::Base>]
-
1
def self.load_paths
-
2
@load_paths ||= if ENV['SASS_PATH']
-
ENV['SASS_PATH'].split(SassC::Util.windows? ? ';' : ':')
-
else
-
1
[]
-
end
-
end
-
end
-
-
1
require_relative "sassc/version"
-
1
require_relative "sassc/native"
-
1
require_relative "sassc/import_handler"
-
1
require_relative "sassc/importer"
-
1
require_relative "sassc/util"
-
1
require_relative "sassc/util/normalized_map"
-
1
require_relative "sassc/script"
-
1
require_relative "sassc/script/value"
-
1
require_relative "sassc/script/value/bool"
-
1
require_relative "sassc/script/value/number"
-
1
require_relative "sassc/script/value/color"
-
1
require_relative "sassc/script/value/string"
-
1
require_relative "sassc/script/value/list"
-
1
require_relative "sassc/script/value/map"
-
1
require_relative "sassc/script/functions"
-
1
require_relative "sassc/script/value_conversion"
-
1
require_relative "sassc/script/value_conversion/base"
-
1
require_relative "sassc/script/value_conversion/string"
-
1
require_relative "sassc/script/value_conversion/number"
-
1
require_relative "sassc/script/value_conversion/color"
-
1
require_relative "sassc/script/value_conversion/map"
-
1
require_relative "sassc/script/value_conversion/list"
-
1
require_relative "sassc/script/value_conversion/bool"
-
1
require_relative "sassc/functions_handler"
-
1
require_relative "sassc/dependency"
-
1
require_relative "sassc/error"
-
1
require_relative "sassc/engine"
-
1
require_relative "sassc/sass_2_scss"
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
class Dependency
-
1
attr_reader :filename
-
1
attr_reader :options
-
-
1
def initialize(filename)
-
8
@filename = filename
-
8
@options = { filename: @filename }
-
end
-
-
1
def self.from_filenames(filenames)
-
9
filenames.map { |f| new(f) }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative "error"
-
-
1
module SassC
-
1
class Engine
-
1
OUTPUT_STYLES = %i[
-
sass_style_nested
-
sass_style_expanded
-
sass_style_compact
-
sass_style_compressed
-
]
-
-
1
attr_reader :template, :options
-
-
1
def initialize(template, options = {})
-
2
@template = template
-
2
@options = options
-
2
@functions = options.fetch(:functions, Script::Functions)
-
end
-
-
1
def render
-
2
return @template.dup if @template.empty?
-
-
2
data_context = Native.make_data_context(@template)
-
2
context = Native.data_context_get_context(data_context)
-
2
native_options = Native.context_get_options(context)
-
-
2
Native.option_set_is_indented_syntax_src(native_options, true) if sass?
-
2
Native.option_set_input_path(native_options, filename) if filename
-
2
Native.option_set_precision(native_options, precision) if precision
-
2
Native.option_set_include_path(native_options, load_paths)
-
2
Native.option_set_output_style(native_options, output_style_enum)
-
2
Native.option_set_source_comments(native_options, true) if line_comments?
-
2
Native.option_set_source_map_file(native_options, source_map_file) if source_map_file
-
2
Native.option_set_source_map_embed(native_options, true) if source_map_embed?
-
2
Native.option_set_source_map_contents(native_options, true) if source_map_contents?
-
2
Native.option_set_omit_source_map_url(native_options, true) if omit_source_map_url?
-
-
2
import_handler.setup(native_options)
-
2
functions_handler.setup(native_options, functions: @functions)
-
-
2
status = Native.compile_data_context(data_context)
-
-
2
if status != 0
-
message = Native.context_get_error_message(context)
-
filename = Native.context_get_error_file(context)
-
line = Native.context_get_error_line(context)
-
-
raise SyntaxError.new(message, filename: filename, line: line)
-
end
-
-
2
css = Native.context_get_output_string(context)
-
-
2
@dependencies = Native.context_get_included_files(context)
-
2
@source_map = Native.context_get_source_map_string(context)
-
-
2
css.force_encoding(@template.encoding)
-
2
@source_map.force_encoding(@template.encoding) if @source_map.is_a?(String)
-
-
2
return css unless quiet?
-
ensure
-
2
Native.delete_data_context(data_context) if data_context
-
end
-
-
1
def dependencies
-
1
raise NotRenderedError unless @dependencies
-
1
Dependency.from_filenames(@dependencies)
-
end
-
-
1
def source_map
-
2
raise NotRenderedError unless @source_map
-
2
@source_map
-
end
-
-
1
def filename
-
4
@options[:filename]
-
end
-
-
1
private
-
-
1
def quiet?
-
2
@options[:quiet]
-
end
-
-
1
def precision
-
2
@options[:precision]
-
end
-
-
1
def sass?
-
2
@options[:syntax] && @options[:syntax].to_sym == :sass
-
end
-
-
1
def line_comments?
-
2
@options[:line_comments]
-
end
-
-
1
def source_map_embed?
-
2
@options[:source_map_embed]
-
end
-
-
1
def source_map_contents?
-
2
@options[:source_map_contents]
-
end
-
-
1
def omit_source_map_url?
-
2
@options[:omit_source_map_url]
-
end
-
-
1
def source_map_file
-
4
@options[:source_map_file]
-
end
-
-
1
def import_handler
-
2
@import_handler ||= ImportHandler.new(@options)
-
end
-
-
1
def functions_handler
-
2
@functions_handler = FunctionsHandler.new(@options)
-
end
-
-
1
def output_style_enum
-
2
@output_style_enum ||= Native::SassOutputStyle[output_style]
-
end
-
-
1
def output_style
-
2
@output_style ||= begin
-
2
style = @options.fetch(:style, :sass_style_nested).to_s
-
2
style = "sass_style_#{style}" unless style.include?("sass_style_")
-
2
style = style.to_sym
-
2
raise InvalidStyleError unless Native::SassOutputStyle.symbols.include?(style)
-
2
style
-
end
-
end
-
-
1
def load_paths
-
2
paths = (@options[:load_paths] || []) + SassC.load_paths
-
2
paths.join(File::PATH_SEPARATOR) unless paths.empty?
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "pathname"
-
-
1
module SassC
-
-
1
class BaseError < StandardError; end
-
1
class NotRenderedError < BaseError; end
-
1
class InvalidStyleError < BaseError; end
-
1
class UnsupportedValue < BaseError; end
-
-
# When dealing with SyntaxErrors,
-
# it's important to provide filename and line number information.
-
# This will be used in various error reports to users, including backtraces.
-
-
1
class SyntaxError < BaseError
-
-
1
def initialize(message, filename: nil, line: nil)
-
@filename = filename
-
@line = line
-
super(message)
-
end
-
-
1
def backtrace
-
return nil if super.nil?
-
sass_backtrace + super
-
end
-
-
# The backtrace of the error within Sass files.
-
1
def sass_backtrace
-
return [] unless @filename && @line
-
["#{@filename}:#{@line}"]
-
end
-
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
class FunctionsHandler
-
1
def initialize(options)
-
2
@options = options
-
end
-
-
1
def setup(native_options, functions: Script::Functions)
-
2
@callbacks = {}
-
2
@function_names = {}
-
-
2
list = Native.make_function_list(Script.custom_functions(functions: functions).count)
-
-
# use an anonymous class wrapper to avoid mutations in a threaded environment
-
2
functions_wrapper = Class.new do
-
2
attr_accessor :options
-
2
include functions
-
end.new
-
2
functions_wrapper.options = @options
-
-
2
Script.custom_functions(functions: functions).each_with_index do |custom_function, i|
-
18
@callbacks[custom_function] = FFI::Function.new(:pointer, [:pointer, :pointer]) do |native_argument_list, cookie|
-
begin
-
function_arguments = arguments_from_native_list(native_argument_list)
-
result = functions_wrapper.send(custom_function, *function_arguments)
-
to_native_value(result)
-
rescue StandardError => exception
-
# This rescues any exceptions that occur either in value conversion
-
# or during the execution of a custom function.
-
error(exception.message)
-
end
-
end
-
-
18
@function_names[custom_function] = Script.formatted_function_name(custom_function, functions: functions)
-
-
18
callback = Native.make_function(
-
@function_names[custom_function],
-
@callbacks[custom_function],
-
nil
-
)
-
-
18
Native::function_set_list_entry(list, i, callback)
-
end
-
-
2
Native::option_set_c_functions(native_options, list)
-
end
-
-
1
private
-
-
1
def arguments_from_native_list(native_argument_list)
-
native_argument_list_length = Native.list_get_length(native_argument_list)
-
-
(0...native_argument_list_length).map do |i|
-
native_value = Native.list_get_value(native_argument_list, i)
-
Script::ValueConversion.from_native(native_value, @options)
-
end.compact
-
end
-
-
1
def to_native_value(sass_value)
-
# if the custom function returns nil, we provide a "default" return
-
# value of an empty string
-
sass_value ||= SassC::Script::Value::String.new("")
-
sass_value.options = @options
-
Script::ValueConversion.to_native(sass_value)
-
end
-
-
1
def error(message)
-
$stderr.puts "[SassC::FunctionsHandler] #{message}"
-
Native.make_error(message)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
class ImportHandler
-
1
def initialize(options)
-
2
@importer = if options[:importer]
-
options[:importer].new(options)
-
else
-
2
nil
-
end
-
end
-
-
1
def setup(native_options)
-
2
return unless @importer
-
-
importer_callback = Native.make_importer(import_function, nil)
-
-
list = Native.make_function_list(1)
-
Native::function_set_list_entry(list, 0, importer_callback)
-
-
Native.option_set_c_importers(native_options, list)
-
end
-
-
1
private
-
-
1
def import_function
-
@import_function ||= FFI::Function.new(:pointer, [:string, :pointer, :pointer]) do |path, importer_entry, compiler|
-
last_import = Native::compiler_get_last_import(compiler)
-
parent_path = Native::import_get_abs_path(last_import)
-
-
imports = [*@importer.imports(path, parent_path)]
-
imports_to_native(imports)
-
end
-
end
-
-
1
def imports_to_native(imports)
-
import_list = Native.make_import_list(imports.size)
-
-
imports.each_with_index do |import, i|
-
source = import.source ? Native.native_string(import.source) : nil
-
source_map_path = nil
-
-
entry = Native.make_import_entry(import.path, source, source_map_path)
-
Native.import_set_list_entry(import_list, i, entry)
-
end
-
-
import_list
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
class Importer
-
1
attr_reader :options
-
-
1
def initialize(options)
-
@options = options
-
end
-
-
1
def imports(path, parent_path)
-
# A custom importer must override this method.
-
# Custom importer may return an Import, or an array of Imports.
-
raise NotImplementedError
-
end
-
-
1
class Import
-
1
attr_accessor :path, :source, :source_map_path
-
-
1
def initialize(path, source: nil, source_map_path: nil)
-
@path = path
-
@source = source
-
@source_map_path = source_map_path
-
end
-
-
1
def to_s
-
"Import: #{path} #{source} #{source_map_path}"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "ffi"
-
-
1
module SassC
-
1
module Native
-
1
extend FFI::Library
-
-
1
dl_ext = RbConfig::MAKEFILE_CONFIG['DLEXT']
-
begin
-
1
ffi_lib File.expand_path("libsass.#{dl_ext}", __dir__)
-
rescue LoadError # Some non-rvm environments don't copy a shared object over to lib/sassc
-
ffi_lib File.expand_path("libsass.#{dl_ext}", "#{__dir__}/../../ext")
-
end
-
-
1
require_relative "native/sass_value"
-
-
1
typedef :pointer, :sass_options_ptr
-
1
typedef :pointer, :sass_context_ptr
-
1
typedef :pointer, :sass_file_context_ptr
-
1
typedef :pointer, :sass_data_context_ptr
-
-
1
typedef :pointer, :sass_c_function_list_ptr
-
1
typedef :pointer, :sass_c_function_callback_ptr
-
1
typedef :pointer, :sass_value_ptr
-
-
1
typedef :pointer, :sass_import_list_ptr
-
1
typedef :pointer, :sass_importer
-
1
typedef :pointer, :sass_import_ptr
-
-
1
callback :sass_c_function, [:pointer, :pointer], :pointer
-
1
callback :sass_c_import_function, [:pointer, :pointer, :pointer], :pointer
-
-
1
require_relative "native/sass_input_style"
-
1
require_relative "native/sass_output_style"
-
1
require_relative "native/string_list"
-
-
# Remove the redundant "sass_" from the beginning of every method name
-
1
def self.attach_function(*args)
-
98
return super if args.size != 3
-
-
95
if args[0] =~ /^sass_/
-
94
args.unshift args[0].to_s.sub(/^sass_/, "")
-
end
-
-
95
super(*args)
-
end
-
-
# https://github.com/ffi/ffi/wiki/Examples#array-of-strings
-
1
def self.return_string_array(ptr)
-
2
ptr.null? ? [] : ptr.get_array_of_string(0).compact
-
end
-
-
1
def self.native_string(string)
-
2
m = FFI::MemoryPointer.from_string(string)
-
2
m.autorelease = false
-
2
m
-
end
-
-
1
require_relative "native/native_context_api"
-
1
require_relative "native/native_functions_api"
-
1
require_relative "native/sass2scss_api"
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
module Native
-
1
attach_function :version, :libsass_version, [], :string
-
-
# Create and initialize an option struct
-
# ADDAPI struct Sass_Options* ADDCALL sass_make_options (void);
-
1
attach_function :sass_make_options, [], :sass_options_ptr
-
-
# Create and initialize a specific context
-
# ADDAPI struct Sass_File_Context* ADDCALL sass_make_file_context (const char* input_path);
-
# ADDAPI struct Sass_Data_Context* ADDCALL sass_make_data_context (char* source_string);
-
1
attach_function :sass_make_file_context, [:string], :sass_file_context_ptr
-
1
attach_function :_make_data_context, :sass_make_data_context, [:pointer], :sass_data_context_ptr
-
-
1
def self.make_data_context(data)
-
2
_make_data_context(Native.native_string(data))
-
end
-
-
# Call the compilation step for the specific context
-
# ADDAPI int ADDCALL sass_compile_file_context (struct Sass_File_Context* ctx);
-
# ADDAPI int ADDCALL sass_compile_data_context (struct Sass_Data_Context* ctx);
-
1
attach_function :sass_compile_file_context, [:sass_file_context_ptr], :int
-
1
attach_function :sass_compile_data_context, [:sass_data_context_ptr], :int
-
-
# Create a sass compiler instance for more control
-
# ADDAPI struct Sass_Compiler* ADDCALL sass_make_file_compiler (struct Sass_File_Context* file_ctx);
-
# ADDAPI struct Sass_Compiler* ADDCALL sass_make_data_compiler (struct Sass_Data_Context* data_ctx);
-
-
# Execute the different compilation steps individually
-
# Usefull if you only want to query the included files
-
# ADDAPI int ADDCALL sass_compiler_parse(struct Sass_Compiler* compiler);
-
# ADDAPI int ADDCALL sass_compiler_execute(struct Sass_Compiler* compiler);
-
-
# Release all memory allocated with the compiler
-
# This does _not_ include any contexts or options
-
# ADDAPI void ADDCALL sass_delete_compiler(struct Sass_Compiler* compiler);
-
-
# Release all memory allocated and also ourself
-
# ADDAPI void ADDCALL sass_delete_file_context (struct Sass_File_Context* ctx);
-
# ADDAPI void ADDCALL sass_delete_data_context (struct Sass_Data_Context* ctx);
-
1
attach_function :sass_delete_file_context, [:sass_file_context_ptr], :void
-
1
attach_function :sass_delete_data_context, [:sass_data_context_ptr], :void
-
-
# Getters for context from specific implementation
-
# ADDAPI struct Sass_Context* ADDCALL sass_file_context_get_context (struct Sass_File_Context* file_ctx);
-
# ADDAPI struct Sass_Context* ADDCALL sass_data_context_get_context (struct Sass_Data_Context* data_ctx);
-
1
attach_function :sass_file_context_get_context, [:sass_file_context_ptr], :sass_context_ptr
-
1
attach_function :sass_data_context_get_context, [:sass_data_context_ptr], :sass_context_ptr
-
-
# Getters for context options from Sass_Context
-
# ADDAPI struct Sass_Options* ADDCALL sass_context_get_options (struct Sass_Context* ctx);
-
# ADDAPI struct Sass_Options* ADDCALL sass_file_context_get_options (struct Sass_File_Context* file_ctx);
-
# ADDAPI struct Sass_Options* ADDCALL sass_data_context_get_options (struct Sass_Data_Context* data_ctx);
-
# ADDAPI void ADDCALL sass_file_context_set_options (struct Sass_File_Context* file_ctx, struct Sass_Options* opt);
-
# ADDAPI void ADDCALL sass_data_context_set_options (struct Sass_Data_Context* data_ctx, struct Sass_Options* opt);
-
1
attach_function :sass_context_get_options, [:sass_context_ptr], :sass_options_ptr
-
1
attach_function :sass_file_context_get_options, [:sass_file_context_ptr], :sass_options_ptr
-
1
attach_function :sass_data_context_get_options, [:sass_data_context_ptr], :sass_options_ptr
-
1
attach_function :sass_file_context_set_options, [:sass_file_context_ptr, :sass_options_ptr], :void
-
1
attach_function :sass_data_context_set_options, [:sass_data_context_ptr, :sass_options_ptr], :void
-
-
# Getters for options
-
# ADDAPI int ADDCALL sass_option_get_precision (struct Sass_Options* options);
-
# ADDAPI enum Sass_Output_Style ADDCALL sass_option_get_output_style (struct Sass_Options* options);
-
# ADDAPI bool ADDCALL sass_option_get_source_comments (struct Sass_Options* options);
-
# ADDAPI bool ADDCALL sass_option_get_source_map_embed (struct Sass_Options* options);
-
# ADDAPI bool ADDCALL sass_option_get_source_map_contents (struct Sass_Options* options);
-
# ADDAPI bool ADDCALL sass_option_get_omit_source_map_url (struct Sass_Options* options);
-
# ADDAPI bool ADDCALL sass_option_get_is_indented_syntax_src (struct Sass_Options* options);
-
# ADDAPI const char* ADDCALL sass_option_get_input_path (struct Sass_Options* options);
-
# ADDAPI const char* ADDCALL sass_option_get_output_path (struct Sass_Options* options);
-
# ADDAPI const char* ADDCALL sass_option_get_include_path (struct Sass_Options* options);
-
# ADDAPI const char* ADDCALL sass_option_get_source_map_file (struct Sass_Options* options);
-
# ADDAPI Sass_C_Function_List ADDCALL sass_option_get_c_functions (struct Sass_Options* options);
-
1
attach_function :sass_option_get_precision, [:sass_options_ptr], :int
-
1
attach_function :sass_option_get_output_style, [:sass_options_ptr], SassOutputStyle
-
1
attach_function :sass_option_get_source_comments, [:sass_options_ptr], :bool
-
1
attach_function :sass_option_get_source_map_embed, [:sass_options_ptr], :bool
-
1
attach_function :sass_option_get_source_map_contents, [:sass_options_ptr], :bool
-
1
attach_function :sass_option_get_omit_source_map_url, [:sass_options_ptr], :bool
-
1
attach_function :sass_option_get_is_indented_syntax_src, [:sass_options_ptr], :bool
-
1
attach_function :sass_option_get_input_path, [:sass_options_ptr], :string
-
1
attach_function :sass_option_get_output_path, [:sass_options_ptr], :string
-
1
attach_function :sass_option_get_include_path, [:sass_options_ptr], :string
-
1
attach_function :sass_option_get_source_map_file, [:sass_options_ptr], :string
-
1
attach_function :sass_option_get_c_functions, [:sass_options_ptr], :sass_c_function_list_ptr
-
# ADDAPI Sass_C_Import_Callback ADDCALL sass_option_get_importer (struct Sass_Options* options);
-
-
# Setters for options
-
# ADDAPI void ADDCALL sass_option_set_precision (struct Sass_Options* options, int precision);
-
# ADDAPI void ADDCALL sass_option_set_output_style (struct Sass_Options* options, enum Sass_Output_Style output_style);
-
# ADDAPI void ADDCALL sass_option_set_source_comments (struct Sass_Options* options, bool source_comments);
-
# ADDAPI void ADDCALL sass_option_set_source_map_embed (struct Sass_Options* options, bool source_map_embed);
-
# ADDAPI void ADDCALL sass_option_set_source_map_contents (struct Sass_Options* options, bool source_map_contents);
-
# ADDAPI void ADDCALL sass_option_set_omit_source_map_url (struct Sass_Options* options, bool omit_source_map_url);
-
# ADDAPI void ADDCALL sass_option_set_is_indented_syntax_src (struct Sass_Options* options, bool is_indented_syntax_src);
-
# ADDAPI void ADDCALL sass_option_set_input_path (struct Sass_Options* options, const char* input_path);
-
# ADDAPI void ADDCALL sass_option_set_output_path (struct Sass_Options* options, const char* output_path);
-
# ADDAPI void ADDCALL sass_option_set_include_path (struct Sass_Options* options, const char* include_path);
-
# ADDAPI void ADDCALL sass_option_set_source_map_file (struct Sass_Options* options, const char* source_map_file);
-
# ADDAPI void ADDCALL sass_option_set_c_functions (struct Sass_Options* options, Sass_C_Function_List c_functions);
-
# ADDAPI void ADDCALL sass_option_set_c_importers (struct Sass_Options* options, Sass_Importer_List c_importers);
-
1
attach_function :sass_option_set_precision, [:sass_options_ptr, :int], :void
-
1
attach_function :sass_option_set_output_style, [:sass_options_ptr, SassOutputStyle], :void
-
1
attach_function :sass_option_set_source_comments, [:sass_options_ptr, :bool], :void
-
1
attach_function :sass_option_set_source_map_embed, [:sass_options_ptr, :bool], :void
-
1
attach_function :sass_option_set_source_map_contents, [:sass_options_ptr, :bool], :void
-
1
attach_function :sass_option_set_omit_source_map_url, [:sass_options_ptr, :bool], :void
-
1
attach_function :sass_option_set_is_indented_syntax_src, [:sass_options_ptr, :bool], :void
-
1
attach_function :sass_option_set_input_path, [:sass_options_ptr, :string], :void
-
1
attach_function :sass_option_set_output_path, [:sass_options_ptr, :string], :void
-
1
attach_function :sass_option_set_include_path, [:sass_options_ptr, :string], :void
-
1
attach_function :sass_option_set_source_map_file, [:sass_options_ptr, :string], :void
-
1
attach_function :sass_option_set_c_functions, [:sass_options_ptr, :pointer], :void
-
1
attach_function :sass_option_set_c_importers, [:sass_options_ptr, :pointer], :void
-
#attach_function :sass_option_set_c_importers, [:sass_options_ptr, :sass_importer], :void
-
-
# Getter for context
-
# ADDAPI const char* ADDCALL sass_context_get_output_string (struct Sass_Context* ctx);
-
# ADDAPI int ADDCALL sass_context_get_error_status (struct Sass_Context* ctx);
-
# ADDAPI const char* ADDCALL sass_context_get_error_json (struct Sass_Context* ctx);
-
# ADDAPI const char* ADDCALL sass_context_get_error_message (struct Sass_Context* ctx);
-
# ADDAPI const char* ADDCALL sass_context_get_error_file (struct Sass_Context* ctx);
-
# ADDAPI size_t ADDCALL sass_context_get_error_line (struct Sass_Context* ctx);
-
# ADDAPI size_t ADDCALL sass_context_get_error_column (struct Sass_Context* ctx);
-
# ADDAPI const char* ADDCALL sass_context_get_source_map_string (struct Sass_Context* ctx);
-
# ADDAPI char** ADDCALL sass_context_get_included_files (struct Sass_Context* ctx);
-
1
attach_function :sass_context_get_output_string, [:sass_context_ptr], :string
-
1
attach_function :sass_context_get_error_status, [:sass_context_ptr], :int
-
1
attach_function :sass_context_get_error_json, [:sass_context_ptr], :string
-
1
attach_function :sass_context_get_error_message, [:sass_context_ptr], :string
-
1
attach_function :sass_context_get_error_file, [:sass_context_ptr], :string
-
1
attach_function :sass_context_get_error_line, [:sass_context_ptr], :size_t
-
1
attach_function :sass_context_get_error_column, [:sass_context_ptr], :size_t
-
1
attach_function :sass_context_get_source_map_string, [:sass_context_ptr], :string
-
1
attach_function :_context_get_included_files, :sass_context_get_included_files, [:sass_context_ptr], :pointer
-
-
1
def self.context_get_included_files(*args)
-
2
return_string_array _context_get_included_files(*args)
-
end
-
-
# ADDAPI Sass_Import_Entry ADDCALL sass_compiler_get_last_import(struct Sass_Compiler* compiler);
-
1
attach_function :sass_compiler_get_last_import, [:pointer], :pointer
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
module Native
-
# Creators for sass function list and function descriptors
-
# ADDAPI Sass_C_Function_List ADDCALL sass_make_function_list (size_t length);
-
# ADDAPI Sass_C_Function_Callback ADDCALL sass_make_function (const char* signature, Sass_C_Function fn, void* cookie);
-
1
attach_function :sass_make_function_list, [:size_t], :sass_c_function_list_ptr
-
1
attach_function :sass_make_function, [:string, :sass_c_function, :pointer], :sass_c_function_callback_ptr
-
-
# Setters and getters for callbacks on function lists
-
# ADDAPI Sass_C_Function_Callback ADDCALL sass_function_get_list_entry(Sass_C_Function_List list, size_t pos);
-
# ADDAPI void ADDCALL sass_function_set_list_entry(Sass_C_Function_List list, size_t pos, Sass_C_Function_Callback cb);
-
1
attach_function :sass_function_get_list_entry, [:sass_c_function_list_ptr, :size_t], :sass_c_function_callback_ptr
-
1
attach_function :sass_function_set_list_entry, [:sass_c_function_list_ptr, :size_t, :sass_c_function_callback_ptr], :void
-
-
# ADDAPI union Sass_Value* ADDCALL sass_make_number (double val, const char* unit);
-
1
attach_function :sass_make_number, [:double, :string], :sass_value_ptr
-
-
# ADDAPI union Sass_Value* ADDCALL sass_make_string (const char* val);
-
1
attach_function :sass_make_string, [:string], :sass_value_ptr
-
-
# ADDAPI union Sass_Value* ADDCALL sass_make_qstring (const char* val);
-
1
attach_function :sass_make_qstring, [:string], :sass_value_ptr
-
-
# ADDAPI union Sass_Value* ADDCALL sass_make_color (double r, double g, double b, double a);
-
1
attach_function :sass_make_color, [:double, :double, :double, :double], :sass_value_ptr
-
-
# ADDAPI union Sass_Value* ADDCALL sass_make_map (size_t len);
-
1
attach_function :sass_make_map, [:size_t], :sass_value_ptr
-
-
# ADDAPI union Sass_Value* ADDCALL sass_make_list (size_t len, enum Sass_Separator sep)
-
1
attach_function :sass_make_list, [:size_t, SassSeparator], :sass_value_ptr
-
-
# ADDAPI union Sass_Value* ADDCALL sass_make_boolean (boolean val);
-
1
attach_function :sass_make_boolean, [:bool], :sass_value_ptr
-
-
# ADDAPI void ADDCALL sass_map_set_key (union Sass_Value* v, size_t i, union Sass_Value*);
-
1
attach_function :sass_map_set_key, [:sass_value_ptr, :size_t, :sass_value_ptr], :void
-
-
# ADDAPI union Sass_Value* ADDCALL sass_map_get_key (const union Sass_Value* v, size_t i);
-
1
attach_function :sass_map_get_key, [:sass_value_ptr, :size_t], :sass_value_ptr
-
-
# ADDAPI void ADDCALL sass_map_set_value (union Sass_Value* v, size_t i, union Sass_Value*);
-
1
attach_function :sass_map_set_value, [:sass_value_ptr, :size_t, :sass_value_ptr], :void
-
-
# ADDAPI union Sass_Value* ADDCALL sass_map_get_value (const union Sass_Value* v, size_t i);
-
1
attach_function :sass_map_get_value, [:sass_value_ptr, :size_t], :sass_value_ptr
-
-
# ADDAPI size_t ADDCALL sass_map_get_length (const union Sass_Value* v);
-
1
attach_function :sass_map_get_length, [:sass_value_ptr], :size_t
-
-
# ADDAPI union Sass_Value* ADDCALL sass_list_get_value (const union Sass_Value* v, size_t i);
-
1
attach_function :sass_list_get_value, [:sass_value_ptr, :size_t], :sass_value_ptr
-
-
# ADDAPI void ADDCALL sass_list_set_value (union Sass_Value* v, size_t i, union Sass_Value* value);
-
1
attach_function :sass_list_set_value, [:sass_value_ptr, :size_t, :sass_value_ptr], :void
-
-
# ADDAPI size_t ADDCALL sass_list_get_length (const union Sass_Value* v);
-
1
attach_function :sass_list_get_length, [:sass_value_ptr], :size_t
-
-
# ADDAPI union Sass_Value* ADDCALL sass_make_error (const char* msg);
-
1
attach_function :sass_make_error, [:string], :sass_value_ptr
-
-
# ADDAPI enum Sass_Tag ADDCALL sass_value_get_tag (const union Sass_Value* v);
-
1
attach_function :sass_value_get_tag, [:sass_value_ptr], SassTag
-
1
attach_function :sass_value_is_null, [:sass_value_ptr], :bool
-
-
# ADDAPI const char* ADDCALL sass_string_get_value (const union Sass_Value* v);
-
1
attach_function :sass_string_get_value, [:sass_value_ptr], :string
-
-
# ADDAPI bool ADDCALL sass_string_is_quoted(const union Sass_Value* v);
-
1
attach_function :sass_string_is_quoted, [:sass_value_ptr], :bool
-
-
# ADDAPI const char* ADDCALL sass_number_get_value (const union Sass_Value* v);
-
1
attach_function :sass_number_get_value, [:sass_value_ptr], :double
-
-
# ADDAPI const char* ADDCALL sass_number_get_unit (const union Sass_Value* v);
-
1
attach_function :sass_number_get_unit, [:sass_value_ptr], :string
-
-
# ADDAPI const char* ADDCALL sass_boolean_get_value (const union Sass_Value* v);
-
1
attach_function :sass_boolean_get_value, [:sass_value_ptr], :bool
-
-
1
def self.string_get_type(native_value)
-
string_is_quoted(native_value) ? :string : :identifier
-
end
-
-
# ADDAPI double ADDCALL sass_color_get_r (const union Sass_Value* v);
-
# ADDAPI void ADDCALL sass_color_set_r (union Sass_Value* v, double r);
-
# ADDAPI double ADDCALL sass_color_get_g (const union Sass_Value* v);
-
# ADDAPI void ADDCALL sass_color_set_g (union Sass_Value* v, double g);
-
# ADDAPI double ADDCALL sass_color_get_b (const union Sass_Value* v);
-
# ADDAPI void ADDCALL sass_color_set_b (union Sass_Value* v, double b);
-
# ADDAPI double ADDCALL sass_color_get_a (const union Sass_Value* v);
-
# ADDAPI void ADDCALL sass_color_set_a (union Sass_Value* v, double a);
-
1
['r', 'g', 'b', 'a'].each do |color_channel|
-
4
attach_function "sass_color_get_#{color_channel}".to_sym, [:sass_value_ptr], :double
-
4
attach_function "sass_color_set_#{color_channel}".to_sym, [:sass_value_ptr, :double], :void
-
end
-
-
# ADDAPI char* ADDCALL sass_error_get_message (const union Sass_Value* v);
-
# ADDAPI void ADDCALL sass_error_set_message (union Sass_Value* v, char* msg);
-
1
attach_function :sass_error_get_message, [:sass_value_ptr], :string
-
1
attach_function :sass_error_set_message, [:sass_value_ptr, :pointer], :void
-
-
# Getters for custom function descriptors
-
# ADDAPI const char* ADDCALL sass_function_get_signature (Sass_C_Function_Callback fn);
-
# ADDAPI Sass_C_Function ADDCALL sass_function_get_function (Sass_C_Function_Callback fn);
-
# ADDAPI void* ADDCALL sass_function_get_cookie (Sass_C_Function_Callback fn);
-
1
attach_function :sass_function_get_signature, [:sass_c_function_callback_ptr], :string
-
1
attach_function :sass_function_get_function, [:sass_c_function_callback_ptr], :sass_c_function
-
1
attach_function :sass_function_get_cookie, [:sass_c_function_callback_ptr], :pointer
-
-
# Creators for custom importer callback (with some additional pointer)
-
# The pointer is mostly used to store the callback into the actual binding
-
# ADDAPI Sass_C_Import_Callback ADDCALL sass_make_importer (Sass_C_Import_Fn, void* cookie);
-
1
attach_function :sass_make_importer, [:sass_c_import_function, :pointer], :sass_importer
-
-
# Getters for import function descriptors
-
# ADDAPI Sass_C_Import_Fn ADDCALL sass_import_get_function (Sass_C_Import_Callback fn);
-
# ADDAPI void* ADDCALL sass_import_get_cookie (Sass_C_Import_Callback fn);
-
-
# Deallocator for associated memory
-
# ADDAPI void ADDCALL sass_delete_importer (Sass_C_Import_Callback fn);
-
-
# Creator for sass custom importer return argument list
-
# ADDAPI struct Sass_Import** ADDCALL sass_make_import_list (size_t length);
-
1
attach_function :sass_make_import_list, [:size_t], :sass_import_list_ptr
-
-
# Creator for a single import entry returned by the custom importer inside the list
-
# ADDAPI struct Sass_Import* ADDCALL sass_make_import_entry (const char* path, char* source, char* srcmap);
-
# ADDAPI struct Sass_Import* ADDCALL sass_make_import (const char* path, const char* base, char* source, char* srcmap);
-
1
attach_function :sass_make_import_entry, [:string, :pointer, :pointer], :sass_import_ptr
-
-
# Setters to insert an entry into the import list (you may also use [] access directly)
-
# Since we are dealing with pointers they should have a guaranteed and fixed size
-
# ADDAPI void ADDCALL sass_import_set_list_entry (struct Sass_Import** list, size_t idx, struct Sass_Import* entry);
-
1
attach_function :sass_import_set_list_entry, [:sass_import_list_ptr, :size_t, :sass_import_ptr], :void
-
# ADDAPI struct Sass_Import* ADDCALL sass_import_get_list_entry (struct Sass_Import** list, size_t idx);
-
-
# Getters for import entry
-
# ADDAPI const char* ADDCALL sass_import_get_imp_path (struct Sass_Import*);
-
1
attach_function :sass_import_get_imp_path, [:sass_import_ptr], :string
-
# ADDAPI const char* ADDCALL sass_import_get_abs_path (struct Sass_Import*);
-
1
attach_function :sass_import_get_abs_path, [:sass_import_ptr], :string
-
# ADDAPI const char* ADDCALL sass_import_get_source (struct Sass_Import*);
-
1
attach_function :sass_import_get_source, [:sass_import_ptr], :string
-
# ADDAPI const char* ADDCALL sass_import_get_srcmap (struct Sass_Import*);
-
# Explicit functions to take ownership of these items
-
# The property on our struct will be reset to NULL
-
# ADDAPI char* ADDCALL sass_import_take_source (struct Sass_Import*);
-
# ADDAPI char* ADDCALL sass_import_take_srcmap (struct Sass_Import*);
-
-
# Deallocator for associated memory (incl. entries)
-
# ADDAPI void ADDCALL sass_delete_import_list (struct Sass_Import**);
-
# Just in case we have some stray import structs
-
# ADDAPI void ADDCALL sass_delete_import (struct Sass_Import*);
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
module Native
-
# ADDAPI char* ADDCALL sass2scss (const char* sass, const int options);
-
1
attach_function :sass2scss, [:string, :int], :string
-
-
# ADDAPI const char* ADDCALL sass2scss_version(void);
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
module Native
-
1
SassInputStyle = enum(
-
:sass_context_null,
-
:sass_context_file,
-
:sass_context_data,
-
:sass_context_folder
-
)
-
end
-
end
-
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
module Native
-
1
SassOutputStyle = enum(
-
:sass_style_nested,
-
:sass_style_expanded,
-
:sass_style_compact,
-
:sass_style_compressed
-
)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
module Native
-
1
class SassValue < FFI::Union; end
-
-
1
SassTag = enum(
-
:sass_boolean,
-
:sass_number,
-
:sass_color,
-
:sass_string,
-
:sass_list,
-
:sass_map,
-
:sass_null,
-
:sass_error,
-
:sass_warning
-
)
-
-
1
SassSeparator = enum(
-
:sass_comma,
-
:sass_space
-
)
-
-
1
class SassUnknown < FFI::Struct
-
1
layout :tag, SassTag
-
end
-
-
1
class SassBoolean < FFI::Struct
-
1
layout :tag, SassTag,
-
:value, :bool
-
end
-
-
1
class SassNumber < FFI::Struct
-
1
layout :tag, SassTag,
-
:value, :double,
-
:unit, :string
-
end
-
-
1
class SassColor < FFI::Struct
-
1
layout :tag, SassTag,
-
:r, :double,
-
:g, :double,
-
:b, :double,
-
:a, :double
-
end
-
-
1
class SassString < FFI::Struct
-
1
layout :tag, SassTag,
-
:value, :string
-
end
-
-
1
class SassList < FFI::Struct
-
1
layout :tag, SassTag,
-
:separator, SassSeparator,
-
:length, :size_t,
-
:values, :pointer
-
end
-
-
1
class SassMapPair < FFI::Struct
-
1
layout :key, SassValue.ptr,
-
:value, SassValue.ptr
-
end
-
-
1
class SassMap < FFI::Struct
-
1
layout :tag, SassTag,
-
:length, :size_t,
-
:pairs, SassMapPair.ptr
-
end
-
-
1
class SassNull < FFI::Struct
-
1
layout :tag, SassTag
-
end
-
-
1
class SassError < FFI::Struct
-
1
layout :tag, SassTag,
-
:message, :string
-
end
-
-
1
class SassWarning < FFI::Struct
-
1
layout :tag, SassTag,
-
:message, :string
-
end
-
-
1
class SassValue # < FFI::Union
-
1
layout :unknown, SassUnknown,
-
:boolean, SassBoolean,
-
:number, SassNumber,
-
:color, SassColor,
-
:string, SassString,
-
:list, SassList,
-
:map, SassMap,
-
:null, SassNull,
-
:error, SassError,
-
:warning, SassWarning
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
module Native
-
1
class StringList < FFI::Struct
-
1
layout :string_list, StringList.ptr,
-
:string, :string
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
class Sass2Scss
-
1
def self.convert(sass)
-
Native.sass2scss(sass, 0)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
module Script
-
-
1
def self.custom_functions(functions: Functions)
-
4
functions.public_instance_methods
-
end
-
-
1
def self.formatted_function_name(function_name, functions: Functions)
-
18
params = functions.instance_method(function_name).parameters
-
35
params = params.map { |param_type, name| "$#{name}#{': null' if param_type == :opt}" }.join(", ")
-
18
return "#{function_name}(#{params})"
-
end
-
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
module Script
-
1
module Functions
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# The abstract superclass for SassScript objects.
-
# Many of these methods, especially the ones that correspond to SassScript operations,
-
# are designed to be overridden by subclasses which may change the semantics somewhat.
-
# The operations listed here are just the defaults.
-
-
1
class SassC::Script::Value
-
-
# Returns the pure Ruby value of the value.
-
# The type of this value varies based on the subclass.
-
1
attr_reader :value
-
-
# The source range in the document on which this node appeared.
-
1
attr_accessor :source_range
-
-
# Creates a new value.
-
1
def initialize(value = nil)
-
2
value.freeze unless value.nil? || value == true || value == false
-
2
@value = value
-
2
@options = nil
-
end
-
-
# Sets the options hash for this node,
-
# as well as for all child nodes.
-
# See the official Sass reference for options.
-
1
attr_writer :options
-
-
# Returns the options hash for this node.
-
# Raises SassC::SyntaxError if the value was created
-
# outside of the parser and \{#to\_s} was called on it
-
1
def options
-
return @options if @options
-
raise SassC::SyntaxError.new("The #options attribute is not set on this #{self.class}. This error is probably occurring because #to_s was called on this value within a custom Sass function without first setting the #options attribute.")
-
end
-
-
# Returns the hash code of this value. Two objects' hash codes should be
-
# equal if the objects are equal.
-
1
def hash
-
value.hash
-
end
-
-
# True if this Value is the same as `other`
-
1
def eql?(other)
-
self == other
-
end
-
-
# Returns a system inspect value for this object
-
1
def inspect
-
value.inspect
-
end
-
-
# Returns `true` (all Values are truthy)
-
1
def to_bool
-
true
-
end
-
-
# Compares this object to `other`
-
1
def ==(other)
-
self.class == other.class && value == other.value
-
end
-
-
# Returns the integer value of this value.
-
# Raises SassC::SyntaxError if this value doesn’t implment integer conversion.
-
1
def to_i
-
raise SassC::SyntaxError.new("#{inspect} is not an integer.")
-
end
-
-
# @raise [SassC::SyntaxError] if this value isn't an integer
-
1
def assert_int!; to_i; end
-
-
# Returns the separator for this value. For non-list-like values or the
-
# empty list, this will be `nil`. For lists or maps, it will be `:space` or `:comma`.
-
1
def separator
-
nil
-
end
-
-
# Whether the value is surrounded by square brackets. For non-list values,
-
# this will be `false`.
-
1
def bracketed
-
false
-
end
-
-
# Returns the value of this Value as an array.
-
# Single Values are considered the same as single-element arrays.
-
1
def to_a
-
[self]
-
end
-
-
# Returns the value of this value as a hash. Most values don't have hash
-
# representations, but [Map]s and empty [List]s do.
-
#
-
# @return [Hash<Value, Value>] This value as a hash
-
# @raise [SassC::SyntaxError] if this value doesn't have a hash representation
-
1
def to_h
-
raise SassC::SyntaxError.new("#{inspect} is not a map.")
-
end
-
-
# Returns the string representation of this value
-
# as it would be output to the CSS document.
-
#
-
# @options opts :quote [String]
-
# The preferred quote style for quoted strings. If `:none`, strings are
-
# always emitted unquoted.
-
# @return [String]
-
1
def to_s(opts = {})
-
SassC::Util.abstract(self)
-
end
-
1
alias_method :to_sass, :to_s
-
-
# Returns `false` (all Values are truthy)
-
1
def null?
-
false
-
end
-
-
# Creates a new list containing `contents` but with the same brackets and
-
# separators as this object, when interpreted as a list.
-
#
-
# @param contents [Array<Value>] The contents of the new list.
-
# @param separator [Symbol] The separator of the new list. Defaults to \{#separator}.
-
# @param bracketed [Boolean] Whether the new list is bracketed. Defaults to \{#bracketed}.
-
# @return [Sass::Script::Value::List]
-
1
def with_contents(contents, separator: self.separator, bracketed: self.bracketed)
-
SassC::Script::Value::List.new(contents, separator: separator, bracketed: bracketed)
-
end
-
-
1
protected
-
-
# Evaluates the value.
-
#
-
# @param environment [Sass::Environment] The environment in which to evaluate the SassScript
-
# @return [Value] This value
-
1
def _perform(environment)
-
self
-
end
-
-
end
-
# frozen_string_literal: true
-
-
# A SassScript object representing a boolean (true or false) value.
-
-
1
class SassC::Script::Value::Bool < SassC::Script::Value
-
-
# The true value in SassScript.
-
# This is assigned before new is overridden below so that we use the default implementation.
-
1
TRUE = new(true)
-
-
# The false value in SassScript.
-
# This is assigned before new is overridden below so that we use the default implementation.
-
1
FALSE = new(false)
-
-
# We override object creation so that users of the core API
-
# will not need to know that booleans are specific constants.
-
# Tests `value` for truthiness and returns the TRUE or FALSE constant.
-
1
def self.new(value)
-
value ? TRUE : FALSE
-
end
-
-
# The pure Ruby value of this Boolean
-
1
attr_reader :value
-
1
alias_method :to_bool, :value
-
-
# Returns the string "true" or "false" for this value
-
1
def to_s(opts = {})
-
@value.to_s
-
end
-
1
alias_method :to_sass, :to_s
-
-
end
-
# frozen_string_literal: true
-
-
# A SassScript object representing a CSS color.
-
# This class provides a very bare-bones system for storing a RGB(A) or HSL(A)
-
# color and converting it to a CSS color function.
-
#
-
# If your Sass method accepts a color you will need to perform any
-
# needed color mathematics or transformations yourself.
-
-
1
class SassC::Script::Value::Color < SassC::Script::Value
-
-
1
attr_reader :red
-
1
attr_reader :green
-
1
attr_reader :blue
-
1
attr_reader :hue
-
1
attr_reader :saturation
-
1
attr_reader :lightness
-
1
attr_reader :alpha
-
-
# Creates a new color with (`red`, `green`, `blue`) or (`hue`, `saturation`, `lightness`
-
# values, plus an optional `alpha` transparency value.
-
1
def initialize(red:nil, green:nil, blue:nil, hue:nil, saturation:nil, lightness:nil, alpha:1.0)
-
if red && green && blue && alpha
-
@mode = :rgba
-
@red = SassC::Util.clamp(red.to_i, 0, 255)
-
@green = SassC::Util.clamp(green.to_i, 0, 255)
-
@blue = SassC::Util.clamp(blue.to_i, 0, 255)
-
@alpha = SassC::Util.clamp(alpha.to_f, 0.0, 1.0)
-
elsif hue && saturation && lightness && alpha
-
@mode = :hsla
-
@hue = SassC::Util.clamp(hue.to_i, 0, 360)
-
@saturation = SassC::Util.clamp(saturation.to_i, 0, 100)
-
@lightness = SassC::Util.clamp(lightness.to_i, 0, 100)
-
@alpha = SassC::Util.clamp(alpha.to_f, 0.0, 1.0)
-
else
-
raise SassC::UnsupportedValue, "Unable to determine color configuration for "
-
end
-
end
-
-
# Returns a CSS color declaration in the form
-
# `rgb(…)`, `rgba(…)`, `hsl(…)`, or `hsla(…)`.
-
1
def to_s
-
if rgba? && @alpha == 1.0
-
return "rgb(#{@red}, #{@green}, #{@blue})"
-
elsif rgba?
-
return "rgba(#{@red}, #{@green}, #{@blue}, #{alpha_string})"
-
elsif hsla? && @alpha == 1.0
-
return "hsl(#{@hue}, #{@saturation}%, #{@lightness}%)"
-
else # hsla?
-
return "hsla(#{@hue}, #{@saturation}%, #{@lightness}%, #{alpha_string})"
-
end
-
end
-
-
# True if this color has RGBA values
-
1
def rgba?
-
@mode == :rgba
-
end
-
-
# True if this color has HSLA values
-
1
def hlsa?
-
@mode == :hlsa
-
end
-
-
# Returns the alpha value of this color as a string
-
# and rounded to 8 decimal places.
-
1
def alpha_string
-
alpha.round(8).to_s
-
end
-
-
# Returns the values of this color in an array.
-
# Provided for compatibility between different SassC::Script::Value classes
-
1
def value
-
return [
-
red, green, blue,
-
hue, saturation, lightness,
-
alpha,
-
].compact
-
end
-
-
# True if this Color is equal to `other_color`
-
1
def eql?(other_color)
-
unless other_color.is_a?(self.class)
-
raise ArgumentError, "No implicit conversion of #{other_color.class} to #{self.class}"
-
end
-
self.value == other_color.value
-
end
-
1
alias_method :==, :eql?
-
-
# Returns a numeric value for comparing two Color objects
-
# This method is used internally by the Hash class and is not the same as `.to_h`
-
1
def hash
-
value.hash
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
class SassC::Script::Value::Map < SassC::Script::Value
-
-
# The Ruby hash containing the contents of this map.
-
# @return [Hash<Node, Node>]
-
1
attr_reader :value
-
1
alias_method :to_h, :value
-
-
# Creates a new map.
-
#
-
# @param hash [Hash<Node, Node>]
-
1
def initialize(hash)
-
super(hash)
-
end
-
-
# @see Value#options=
-
1
def options=(options)
-
super
-
value.each do |k, v|
-
k.options = options
-
v.options = options
-
end
-
end
-
-
# @see Value#separator
-
1
def separator
-
:comma unless value.empty?
-
end
-
-
# @see Value#to_a
-
1
def to_a
-
value.map do |k, v|
-
list = SassC::Script::Value::List.new([k, v], separator: :space)
-
list.options = options
-
list
-
end
-
end
-
-
# @see Value#eq
-
1
def eq(other)
-
SassC::Script::Value::Bool.new(other.is_a?(Map) && value == other.value)
-
end
-
-
1
def hash
-
@hash ||= value.hash
-
end
-
-
# @see Value#to_s
-
1
def to_s(opts = {})
-
raise SassC::SyntaxError.new("#{inspect} isn't a valid CSS value.")
-
end
-
-
1
def to_sass(opts = {})
-
return "()" if value.empty?
-
-
to_sass = lambda do |value|
-
if value.is_a?(List) && value.separator == :comma
-
"(#{value.to_sass(opts)})"
-
else
-
value.to_sass(opts)
-
end
-
end
-
-
"(#{value.map {|(k, v)| "#{to_sass[k]}: #{to_sass[v]}"}.join(', ')})"
-
end
-
1
alias_method :inspect, :to_sass
-
-
end
-
# frozen_string_literal: true
-
-
# A SassScript object representing a number.
-
# SassScript numbers can have decimal values,
-
# and can also have units.
-
# For example, `12`, `1px`, and `10.45em`
-
# are all valid values.
-
#
-
# Numbers can also have more complex units, such as `1px*em/in`.
-
# These cannot be inputted directly in Sass code at the moment.
-
-
1
class SassC::Script::Value::Number < SassC::Script::Value
-
-
# The Ruby value of the number.
-
#
-
# @return [Numeric]
-
1
attr_reader :value
-
-
# A list of units in the numerator of the number.
-
# For example, `1px*em/in*cm` would return `["px", "em"]`
-
# @return [Array<String>]
-
1
attr_reader :numerator_units
-
-
# A list of units in the denominator of the number.
-
# For example, `1px*em/in*cm` would return `["in", "cm"]`
-
# @return [Array<String>]
-
1
attr_reader :denominator_units
-
-
# The original representation of this number.
-
# For example, although the result of `1px/2px` is `0.5`,
-
# the value of `#original` is `"1px/2px"`.
-
#
-
# This is only non-nil when the original value should be used as the CSS value,
-
# as in `font: 1px/2px`.
-
#
-
# @return [Boolean, nil]
-
1
attr_accessor :original
-
-
1
def self.precision
-
Thread.current[:sass_numeric_precision] || Thread.main[:sass_numeric_precision] || 10
-
end
-
-
# Sets the number of digits of precision
-
# For example, if this is `3`,
-
# `3.1415926` will be printed as `3.142`.
-
# The numeric precision is stored as a thread local for thread safety reasons.
-
# To set for all threads, be sure to set the precision on the main thread.
-
1
def self.precision=(digits)
-
Thread.current[:sass_numeric_precision] = digits.round
-
Thread.current[:sass_numeric_precision_factor] = nil
-
Thread.current[:sass_numeric_epsilon] = nil
-
end
-
-
# the precision factor used in numeric output
-
# it is derived from the `precision` method.
-
1
def self.precision_factor
-
Thread.current[:sass_numeric_precision_factor] ||= 10.0**precision
-
end
-
-
# Used in checking equality of floating point numbers. Any
-
# numbers within an `epsilon` of each other are considered functionally equal.
-
# The value for epsilon is one tenth of the current numeric precision.
-
1
def self.epsilon
-
Thread.current[:sass_numeric_epsilon] ||= 1 / (precision_factor * 10)
-
end
-
-
# Used so we don't allocate two new arrays for each new number.
-
1
NO_UNITS = []
-
-
# @param value [Numeric] The value of the number
-
# @param numerator_units [::String, Array<::String>] See \{#numerator\_units}
-
# @param denominator_units [::String, Array<::String>] See \{#denominator\_units}
-
1
def initialize(value, numerator_units = NO_UNITS, denominator_units = NO_UNITS)
-
numerator_units = [numerator_units] if numerator_units.is_a?(::String)
-
denominator_units = [denominator_units] if denominator_units.is_a?(::String)
-
super(value)
-
@numerator_units = numerator_units
-
@denominator_units = denominator_units
-
@options = nil
-
normalize!
-
end
-
-
1
def hash
-
[value, numerator_units, denominator_units].hash
-
end
-
-
# Hash-equality works differently than `==` equality for numbers.
-
# Hash-equality must be transitive, so it just compares the exact value,
-
# numerator units, and denominator units.
-
1
def eql?(other)
-
basically_equal?(value, other.value) && numerator_units == other.numerator_units &&
-
denominator_units == other.denominator_units
-
end
-
-
# @return [String] The CSS representation of this number
-
# @raise [Sass::SyntaxError] if this number has units that can't be used in CSS
-
# (e.g. `px*in`)
-
1
def to_s(opts = {})
-
return original if original
-
raise Sass::SyntaxError.new("#{inspect} isn't a valid CSS value.") unless legal_units?
-
inspect
-
end
-
-
# Returns a readable representation of this number.
-
#
-
# This representation is valid CSS (and valid SassScript)
-
# as long as there is only one unit.
-
#
-
# @return [String] The representation
-
1
def inspect(opts = {})
-
return original if original
-
-
value = self.class.round(self.value)
-
str = value.to_s
-
-
# Ruby will occasionally print in scientific notation if the number is
-
# small enough. That's technically valid CSS, but it's not well-supported
-
# and confusing.
-
str = ("%0.#{self.class.precision}f" % value).gsub(/0*$/, '') if str.include?('e')
-
-
# Sometimes numeric formatting will result in a decimal number with a trailing zero (x.0)
-
if str =~ /(.*)\.0$/
-
str = $1
-
end
-
-
# We omit a leading zero before the decimal point in compressed mode.
-
if @options && options[:style] == :compressed
-
str.sub!(/^(-)?0\./, '\1.')
-
end
-
-
unitless? ? str : "#{str}#{unit_str}"
-
end
-
1
alias_method :to_sass, :inspect
-
-
# @return [Integer] The integer value of the number
-
# @raise [Sass::SyntaxError] if the number isn't an integer
-
1
def to_i
-
super unless int?
-
value.to_i
-
end
-
-
# @return [Boolean] Whether or not this number is an integer.
-
1
def int?
-
basically_equal?(value % 1, 0.0)
-
end
-
-
# @return [Boolean] Whether or not this number has no units.
-
1
def unitless?
-
@numerator_units.empty? && @denominator_units.empty?
-
end
-
-
# Checks whether the number has the numerator unit specified.
-
#
-
# @example
-
# number = Sass::Script::Value::Number.new(10, "px")
-
# number.is_unit?("px") => true
-
# number.is_unit?(nil) => false
-
#
-
# @param unit [::String, nil] The unit the number should have or nil if the number
-
# should be unitless.
-
# @see Number#unitless? The unitless? method may be more readable.
-
1
def is_unit?(unit)
-
if unit
-
denominator_units.size == 0 && numerator_units.size == 1 && numerator_units.first == unit
-
else
-
unitless?
-
end
-
end
-
-
# @return [Boolean] Whether or not this number has units that can be represented in CSS
-
# (that is, zero or one \{#numerator\_units}).
-
1
def legal_units?
-
(@numerator_units.empty? || @numerator_units.size == 1) && @denominator_units.empty?
-
end
-
-
# Returns this number converted to other units.
-
# The conversion takes into account the relationship between e.g. mm and cm,
-
# as well as between e.g. in and cm.
-
#
-
# If this number has no units, it will simply return itself
-
# with the given units.
-
#
-
# An incompatible coercion, e.g. between px and cm, will raise an error.
-
#
-
# @param num_units [Array<String>] The numerator units to coerce this number into.
-
# See {\#numerator\_units}
-
# @param den_units [Array<String>] The denominator units to coerce this number into.
-
# See {\#denominator\_units}
-
# @return [Number] The number with the new units
-
# @raise [Sass::UnitConversionError] if the given units are incompatible with the number's
-
# current units
-
1
def coerce(num_units, den_units)
-
Number.new(if unitless?
-
value
-
else
-
value * coercion_factor(@numerator_units, num_units) /
-
coercion_factor(@denominator_units, den_units)
-
end, num_units, den_units)
-
end
-
-
# @param other [Number] A number to decide if it can be compared with this number.
-
# @return [Boolean] Whether or not this number can be compared with the other.
-
1
def comparable_to?(other)
-
operate(other, :+)
-
true
-
rescue Sass::UnitConversionError
-
false
-
end
-
-
# Returns a human readable representation of the units in this number.
-
# For complex units this takes the form of:
-
# numerator_unit1 * numerator_unit2 / denominator_unit1 * denominator_unit2
-
# @return [String] a string that represents the units in this number
-
1
def unit_str
-
rv = @numerator_units.sort.join("*")
-
if @denominator_units.any?
-
rv << "/"
-
rv << @denominator_units.sort.join("*")
-
end
-
rv
-
end
-
-
1
private
-
-
# @private
-
# @see Sass::Script::Number.basically_equal?
-
1
def basically_equal?(num1, num2)
-
self.class.basically_equal?(num1, num2)
-
end
-
-
# Checks whether two numbers are within an epsilon of each other.
-
# @return [Boolean]
-
1
def self.basically_equal?(num1, num2)
-
(num1 - num2).abs < epsilon
-
end
-
-
# @private
-
1
def self.round(num)
-
if num.is_a?(Float) && (num.infinite? || num.nan?)
-
num
-
elsif basically_equal?(num % 1, 0.0)
-
num.round
-
else
-
((num * precision_factor).round / precision_factor).to_f
-
end
-
end
-
-
1
OPERATIONS = [:+, :-, :<=, :<, :>, :>=, :%]
-
-
1
def operate(other, operation)
-
this = self
-
if OPERATIONS.include?(operation)
-
if unitless?
-
this = this.coerce(other.numerator_units, other.denominator_units)
-
else
-
other = other.coerce(@numerator_units, @denominator_units)
-
end
-
end
-
# avoid integer division
-
value = :/ == operation ? this.value.to_f : this.value
-
result = value.send(operation, other.value)
-
-
if result.is_a?(Numeric)
-
Number.new(result, *compute_units(this, other, operation))
-
else # Boolean op
-
Bool.new(result)
-
end
-
end
-
-
1
def coercion_factor(from_units, to_units)
-
# get a list of unmatched units
-
from_units, to_units = sans_common_units(from_units, to_units)
-
-
if from_units.size != to_units.size || !convertable?(from_units | to_units)
-
raise Sass::UnitConversionError.new(
-
"Incompatible units: '#{from_units.join('*')}' and '#{to_units.join('*')}'.")
-
end
-
-
from_units.zip(to_units).inject(1) {|m, p| m * conversion_factor(p[0], p[1])}
-
end
-
-
1
def compute_units(this, other, operation)
-
case operation
-
when :*
-
[this.numerator_units + other.numerator_units,
-
this.denominator_units + other.denominator_units]
-
when :/
-
[this.numerator_units + other.denominator_units,
-
this.denominator_units + other.numerator_units]
-
else
-
[this.numerator_units, this.denominator_units]
-
end
-
end
-
-
1
def normalize!
-
return if unitless?
-
@numerator_units, @denominator_units =
-
sans_common_units(@numerator_units, @denominator_units)
-
-
@denominator_units.each_with_index do |d, i|
-
next unless convertable?(d) && (u = @numerator_units.find {|n| convertable?([n, d])})
-
@value /= conversion_factor(d, u)
-
@denominator_units.delete_at(i)
-
@numerator_units.delete_at(@numerator_units.index(u))
-
end
-
end
-
-
# This is the source data for all the unit logic. It's pre-processed to make
-
# it efficient to figure out whether a set of units is mutually compatible
-
# and what the conversion ratio is between two units.
-
#
-
# These come from http://www.w3.org/TR/2012/WD-css3-values-20120308/.
-
relative_sizes = [
-
{
-
1
"in" => Rational(1),
-
"cm" => Rational(1, 2.54),
-
"pc" => Rational(1, 6),
-
"mm" => Rational(1, 25.4),
-
"q" => Rational(1, 101.6),
-
"pt" => Rational(1, 72),
-
"px" => Rational(1, 96)
-
},
-
{
-
"deg" => Rational(1, 360),
-
"grad" => Rational(1, 400),
-
"rad" => Rational(1, 2 * Math::PI),
-
"turn" => Rational(1)
-
},
-
{
-
"s" => Rational(1),
-
"ms" => Rational(1, 1000)
-
},
-
{
-
"Hz" => Rational(1),
-
"kHz" => Rational(1000)
-
},
-
{
-
"dpi" => Rational(1),
-
"dpcm" => Rational(254, 100),
-
"dppx" => Rational(96)
-
}
-
]
-
-
# A hash from each known unit to the set of units that it's mutually
-
# convertible with.
-
1
MUTUALLY_CONVERTIBLE = {}
-
1
relative_sizes.map do |values|
-
5
set = values.keys.to_set
-
23
values.keys.each {|name| MUTUALLY_CONVERTIBLE[name] = set}
-
end
-
-
# A two-dimensional hash from two units to the conversion ratio between
-
# them. Multiply `X` by `CONVERSION_TABLE[X][Y]` to convert it to `Y`.
-
1
CONVERSION_TABLE = {}
-
1
relative_sizes.each do |values|
-
5
values.each do |(name1, value1)|
-
18
CONVERSION_TABLE[name1] ||= {}
-
18
values.each do |(name2, value2)|
-
82
value = value1 / value2
-
82
CONVERSION_TABLE[name1][name2] = value.denominator == 1 ? value.to_i : value.to_f
-
end
-
end
-
end
-
-
1
def conversion_factor(from_unit, to_unit)
-
CONVERSION_TABLE[from_unit][to_unit]
-
end
-
-
1
def convertable?(units)
-
units = Array(units).to_set
-
return true if units.empty?
-
return false unless (mutually_convertible = MUTUALLY_CONVERTIBLE[units.first])
-
units.subset?(mutually_convertible)
-
end
-
-
1
def sans_common_units(units1, units2)
-
units2 = units2.dup
-
# Can't just use -, because we want px*px to coerce properly to px*mm
-
units1 = units1.map do |u|
-
j = units2.index(u)
-
next u unless j
-
units2.delete_at(j)
-
nil
-
end
-
units1.compact!
-
return units1, units2
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
class SassC::Script::Value::String < SassC::Script::Value
-
-
# The Ruby value of the string.
-
1
attr_reader :value
-
-
# Whether this is a CSS string or a CSS identifier.
-
# The difference is that strings are written with double-quotes,
-
# while identifiers aren't.
-
#
-
# @return [Symbol] `:string` or `:identifier`
-
1
attr_reader :type
-
-
# Returns the quoted string representation of `contents`.
-
#
-
# @options opts :quote [String]
-
# The preferred quote style for quoted strings. If `:none`, strings are
-
# always emitted unquoted. If `nil`, quoting is determined automatically.
-
# @options opts :sass [String]
-
# Whether to quote strings for Sass source, as opposed to CSS. Defaults to `false`.
-
1
def self.quote(contents, opts = {})
-
quote = opts[:quote]
-
-
# Short-circuit if there are no characters that need quoting.
-
unless contents =~ /[\n\\"']|\#\{/
-
quote ||= '"'
-
return "#{quote}#{contents}#{quote}"
-
end
-
-
if quote.nil?
-
if contents.include?('"')
-
if contents.include?("'")
-
quote = '"'
-
else
-
quote = "'"
-
end
-
else
-
quote = '"'
-
end
-
end
-
-
# Replace single backslashes with multiples.
-
contents = contents.gsub("\\", "\\\\\\\\")
-
-
# Escape interpolation.
-
contents = contents.gsub('#{', "\\\#{") if opts[:sass]
-
-
if quote == '"'
-
contents = contents.gsub('"', "\\\"")
-
else
-
contents = contents.gsub("'", "\\'")
-
end
-
-
contents = contents.gsub(/\n(?![a-fA-F0-9\s])/, "\\a").gsub("\n", "\\a ")
-
"#{quote}#{contents}#{quote}"
-
end
-
-
# Creates a new string.
-
#
-
# @param value [String] See \{#value}
-
# @param type [Symbol] See \{#type}
-
# @param deprecated_interp_equivalent [String?]
-
# If this was created via a potentially-deprecated string interpolation,
-
# this is the replacement expression that should be suggested to the user.
-
1
def initialize(value, type = :identifier)
-
super(value)
-
@type = type
-
end
-
-
# @see Value#plus
-
1
def plus(other)
-
if other.is_a?(SassC::Script::Value::String)
-
other_value = other.value
-
else
-
other_value = other.to_s(:quote => :none)
-
end
-
SassC::Script::Value::String.new(value + other_value, type)
-
end
-
-
# @see Value#to_s
-
1
def to_s(opts = {})
-
return @value.gsub(/\n\s*/, ' ') if opts[:quote] == :none || @type == :identifier
-
self.class.quote(value, opts)
-
end
-
-
# @see Value#to_sass
-
1
def to_sass(opts = {})
-
to_s(opts.merge(:sass => true))
-
end
-
-
1
def inspect
-
String.quote(value)
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
module SassC::Script::ValueConversion
-
-
1
def self.from_native(native_value, options)
-
case value_tag = SassC::Native.value_get_tag(native_value)
-
when :sass_null
-
# no-op
-
when :sass_string
-
value = SassC::Native.string_get_value(native_value)
-
type = SassC::Native.string_get_type(native_value)
-
argument = SassC::Script::Value::String.new(value, type)
-
argument
-
when :sass_boolean
-
value = SassC::Native.boolean_get_value(native_value)
-
argument = SassC::Script::Value::Bool.new(value)
-
argument
-
when :sass_number
-
value = SassC::Native.number_get_value(native_value)
-
unit = SassC::Native.number_get_unit(native_value)
-
argument = SassC::Script::Value::Number.new(value, unit)
-
argument
-
when :sass_color
-
red, green, blue, alpha = SassC::Native.color_get_r(native_value), SassC::Native.color_get_g(native_value), SassC::Native.color_get_b(native_value), SassC::Native.color_get_a(native_value)
-
argument = SassC::Script::Value::Color.new(red:red, green:green, blue:blue, alpha:alpha)
-
argument.options = options
-
argument
-
when :sass_map
-
values = {}
-
length = SassC::Native::map_get_length native_value
-
(0..length-1).each do |index|
-
key = SassC::Native::map_get_key(native_value, index)
-
value = SassC::Native::map_get_value(native_value, index)
-
values[from_native(key, options)] = from_native(value, options)
-
end
-
argument = SassC::Script::Value::Map.new values
-
argument
-
when :sass_list
-
length = SassC::Native::list_get_length(native_value)
-
items = (0...length).map do |index|
-
native_item = SassC::Native::list_get_value(native_value, index)
-
from_native(native_item, options)
-
end
-
SassC::Script::Value::List.new(items, separator: :space)
-
else
-
raise UnsupportedValue.new("Sass argument of type #{value_tag} unsupported")
-
end
-
end
-
-
1
def self.to_native(value)
-
case value_name = value.class.name.split("::").last
-
when "String"
-
SassC::Script::ValueConversion::String.new(value).to_native
-
when "Color"
-
SassC::Script::ValueConversion::Color.new(value).to_native
-
when "Number"
-
SassC::Script::ValueConversion::Number.new(value).to_native
-
when "Map"
-
SassC::Script::ValueConversion::Map.new(value).to_native
-
when "List"
-
SassC::Script::ValueConversion::List.new(value).to_native
-
when "Bool"
-
SassC::Script::ValueConversion::Bool.new(value).to_native
-
else
-
raise SassC::UnsupportedValue.new("Sass return type #{value_name} unsupported")
-
end
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
module Script
-
1
module ValueConversion
-
1
class Base
-
1
def initialize(value)
-
@value = value
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
module Script
-
1
module ValueConversion
-
1
class Bool < Base
-
1
def to_native
-
Native::make_boolean(@value.value)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
module Script
-
1
module ValueConversion
-
1
class Color < Base
-
1
def to_native
-
Native::make_color(
-
@value.red,
-
@value.green,
-
@value.blue,
-
@value.alpha
-
)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
module Script
-
1
module ValueConversion
-
1
SEPARATORS = {
-
space: :sass_space,
-
comma: :sass_comma
-
}
-
-
1
class List < Base
-
1
def to_native
-
list = @value.to_a
-
sep = SEPARATORS.fetch(@value.separator)
-
native_list = Native::make_list(list.size, sep)
-
list.each_with_index do |item, index|
-
native_item = ValueConversion.to_native(item)
-
Native::list_set_value(native_list, index, native_item)
-
end
-
native_list
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
module Script
-
1
module ValueConversion
-
1
class Map < Base
-
1
def to_native
-
hash = @value.to_h
-
native_map = Native::make_map( hash.size )
-
hash.each_with_index do |(key, value), index|
-
key = ValueConversion.to_native key
-
value = ValueConversion.to_native value
-
Native::map_set_key( native_map, index, key )
-
Native::map_set_value( native_map, index, value )
-
end
-
return native_map
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
module Script
-
1
module ValueConversion
-
1
class Number < Base
-
1
def to_native
-
Native::make_number(@value.value, @value.numerator_units.first)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
module Script
-
1
module ValueConversion
-
1
class String < Base
-
1
def to_native(opts = {})
-
if opts[:quote] == :none || @value.type == :identifier
-
Native::make_string(@value.to_s)
-
else
-
Native::make_qstring(@value.to_s)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "erb"
-
1
require "set"
-
1
require "enumerator"
-
1
require "stringio"
-
1
require "rbconfig"
-
1
require "uri"
-
1
require "thread"
-
1
require "pathname"
-
-
# A module containing various useful functions.
-
-
1
module SassC::Util
-
-
1
extend self
-
-
# An array of ints representing the Ruby version number.
-
# @api public
-
4
RUBY_VERSION_COMPONENTS = RUBY_VERSION.split(".").map {|s| s.to_i}
-
-
# The Ruby engine we're running under. Defaults to `"ruby"`
-
# if the top-level constant is undefined.
-
# @api public
-
1
RUBY_ENGINE = defined?(::RUBY_ENGINE) ? ::RUBY_ENGINE : "ruby"
-
-
# Maps the keys in a hash according to a block.
-
# @example
-
# map_keys({:foo => "bar", :baz => "bang"}) {|k| k.to_s}
-
# #=> {"foo" => "bar", "baz" => "bang"}
-
# @param hash [Hash] The hash to map
-
# @yield [key] A block in which the keys are transformed
-
# @yieldparam key [Object] The key that should be mapped
-
# @yieldreturn [Object] The new value for the key
-
# @return [Hash] The mapped hash
-
# @see #map_vals
-
# @see #map_hash
-
1
def map_keys(hash)
-
map_hash(hash) {|k, v| [yield(k), v]}
-
end
-
-
# Restricts the numeric `value` to be within `min` and `max`, inclusive.
-
# If the value is lower than `min`
-
1
def clamp(value, min, max)
-
return min if value < min
-
return max if value > max
-
return value
-
end
-
-
# Like [Fixnum.round], but leaves rooms for slight floating-point
-
# differences.
-
#
-
# @param value [Numeric]
-
# @return [Numeric]
-
1
def round(value)
-
# If the number is within epsilon of X.5, round up (or down for negative
-
# numbers).
-
mod = value % 1
-
mod_is_half = (mod - 0.5).abs < SassC::Script::Value::Number.epsilon
-
if value > 0
-
!mod_is_half && mod < 0.5 ? value.floor : value.ceil
-
else
-
mod_is_half || mod < 0.5 ? value.floor : value.ceil
-
end
-
end
-
-
# Return an array of all possible paths through the given arrays.
-
#
-
# @param arrs [Array<Array>]
-
# @return [Array<Arrays>]
-
#
-
# @example
-
# paths([[1, 2], [3, 4], [5]]) #=>
-
# # [[1, 3, 5],
-
# # [2, 3, 5],
-
# # [1, 4, 5],
-
# # [2, 4, 5]]
-
1
def paths(arrs)
-
arrs.inject([[]]) do |paths, arr|
-
arr.map {|e| paths.map {|path| path + [e]}}.flatten(1)
-
end
-
end
-
-
# Returns information about the caller of the previous method.
-
#
-
# @param entry [String] An entry in the `#caller` list, or a similarly formatted string
-
# @return [[String, Integer, (String, nil)]]
-
# An array containing the filename, line, and method name of the caller.
-
# The method name may be nil
-
1
def caller_info(entry = nil)
-
# JRuby evaluates `caller` incorrectly when it's in an actual default argument.
-
entry ||= caller[1]
-
info = entry.scan(/^((?:[A-Za-z]:)?.*?):(-?.*?)(?::.*`(.+)')?$/).first
-
info[1] = info[1].to_i
-
# This is added by Rubinius to designate a block, but we don't care about it.
-
info[2].sub!(/ \{\}\Z/, '') if info[2]
-
info
-
end
-
-
# Throws a NotImplementedError for an abstract method.
-
#
-
# @param obj [Object] `self`
-
# @raise [NotImplementedError]
-
1
def abstract(obj)
-
raise NotImplementedError.new("#{obj.class} must implement ##{caller_info[2]}")
-
end
-
-
# Prints a deprecation warning for the caller method.
-
#
-
# @param obj [Object] `self`
-
# @param message [String] A message describing what to do instead.
-
1
def deprecated(obj, message = nil)
-
obj_class = obj.is_a?(Class) ? "#{obj}." : "#{obj.class}#"
-
full_message = "DEPRECATION WARNING: #{obj_class}#{caller_info[2]} " +
-
"will be removed in a future version of Sass.#{("\n" + message) if message}"
-
SassC::Util.sass_warn full_message
-
end
-
-
# Silences all Sass warnings within a block.
-
#
-
# @yield A block in which no Sass warnings will be printed
-
1
def silence_sass_warnings
-
old_level, Sass.logger.log_level = Sass.logger.log_level, :error
-
yield
-
ensure
-
SassC.logger.log_level = old_level
-
end
-
-
# The same as `Kernel#warn`, but is silenced by \{#silence\_sass\_warnings}.
-
#
-
# @param msg [String]
-
1
def sass_warn(msg)
-
Sass.logger.warn("#{msg}\n")
-
end
-
-
## Cross Rails Version Compatibility
-
-
# Returns the root of the Rails application,
-
# if this is running in a Rails context.
-
# Returns `nil` if no such root is defined.
-
#
-
# @return [String, nil]
-
1
def rails_root
-
if defined?(::Rails.root)
-
return ::Rails.root.to_s if ::Rails.root
-
raise "ERROR: Rails.root is nil!"
-
end
-
return RAILS_ROOT.to_s if defined?(RAILS_ROOT)
-
nil
-
end
-
-
# Returns the environment of the Rails application,
-
# if this is running in a Rails context.
-
# Returns `nil` if no such environment is defined.
-
#
-
# @return [String, nil]
-
1
def rails_env
-
return ::Rails.env.to_s if defined?(::Rails.env)
-
return RAILS_ENV.to_s if defined?(RAILS_ENV)
-
nil
-
end
-
-
## Cross-OS Compatibility
-
#
-
# These methods are cached because some of them are called quite frequently
-
# and even basic checks like String#== are too costly to be called repeatedly.
-
-
# Whether or not this is running on Windows.
-
#
-
# @return [Boolean]
-
1
def windows?
-
return @windows if defined?(@windows)
-
@windows = (RbConfig::CONFIG['host_os'] =~ /mswin|windows|mingw/i)
-
end
-
-
# Whether or not this is running on IronRuby.
-
#
-
# @return [Boolean]
-
1
def ironruby?
-
return @ironruby if defined?(@ironruby)
-
@ironruby = RUBY_ENGINE == "ironruby"
-
end
-
-
# Whether or not this is running on Rubinius.
-
#
-
# @return [Boolean]
-
1
def rbx?
-
return @rbx if defined?(@rbx)
-
@rbx = RUBY_ENGINE == "rbx"
-
end
-
-
# Whether or not this is running on JRuby.
-
#
-
# @return [Boolean]
-
1
def jruby?
-
return @jruby if defined?(@jruby)
-
@jruby = RUBY_PLATFORM =~ /java/
-
end
-
-
# Returns an array of ints representing the JRuby version number.
-
#
-
# @return [Array<Integer>]
-
1
def jruby_version
-
@jruby_version ||= ::JRUBY_VERSION.split(".").map {|s| s.to_i}
-
end
-
-
# Returns `path` relative to `from`.
-
#
-
# This is like `Pathname#relative_path_from` except it accepts both strings
-
# and pathnames, it handles Windows path separators correctly, and it throws
-
# an error rather than crashing if the paths use different encodings
-
# (https://github.com/ruby/ruby/pull/713).
-
#
-
# @param path [String, Pathname]
-
# @param from [String, Pathname]
-
# @return [Pathname?]
-
1
def relative_path_from(path, from)
-
pathname(path.to_s).relative_path_from(pathname(from.to_s))
-
rescue NoMethodError => e
-
raise e unless e.name == :zero?
-
-
# Work around https://github.com/ruby/ruby/pull/713.
-
path = path.to_s
-
from = from.to_s
-
raise ArgumentError("Incompatible path encodings: #{path.inspect} is #{path.encoding}, " +
-
"#{from.inspect} is #{from.encoding}")
-
end
-
-
18
singleton_methods.each {|method| module_function method}
-
-
end
-
# frozen_string_literal: true
-
-
1
require "delegate"
-
-
# A hash that normalizes its string keys while still allowing you to get back
-
# to the original keys that were stored. If several different values normalize
-
# to the same value, whichever is stored last wins.
-
-
1
class SassC::Util::NormalizedMap
-
-
# Create a normalized map
-
1
def initialize(map = nil)
-
@key_strings = {}
-
@map = {}
-
map.each {|key, value| self[key] = value} if map
-
end
-
-
# Specifies how to transform the key.
-
# This can be overridden to create other normalization behaviors.
-
1
def normalize(key)
-
key.tr("-", "_")
-
end
-
-
# Returns the version of `key` as it was stored before
-
# normalization. If `key` isn't in the map, returns it as it was
-
# passed in.
-
# @return [String]
-
1
def denormalize(key)
-
@key_strings[normalize(key)] || key
-
end
-
-
# @private
-
1
def []=(k, v)
-
normalized = normalize(k)
-
@map[normalized] = v
-
@key_strings[normalized] = k
-
v
-
end
-
-
# @private
-
1
def [](k)
-
@map[normalize(k)]
-
end
-
-
# @private
-
1
def has_key?(k)
-
@map.has_key?(normalize(k))
-
end
-
-
# @private
-
1
def delete(k)
-
normalized = normalize(k)
-
@key_strings.delete(normalized)
-
@map.delete(normalized)
-
end
-
-
# @return [Hash] Hash with the keys as they were stored (before normalization).
-
1
def as_stored
-
SassC::Util.map_keys(@map) {|k| @key_strings[k]}
-
end
-
-
1
def empty?
-
@map.empty?
-
end
-
-
1
def values
-
@map.values
-
end
-
-
1
def keys
-
@map.keys
-
end
-
-
1
def each
-
@map.each {|k, v| yield(k, v)}
-
end
-
-
1
def size
-
@map.size
-
end
-
-
1
def to_hash
-
@map.dup
-
end
-
-
1
def to_a
-
@map.to_a
-
end
-
-
1
def map
-
@map.map {|k, v| yield(k, v)}
-
end
-
-
1
def dup
-
d = super
-
d.send(:instance_variable_set, "@map", @map.dup)
-
d
-
end
-
-
1
def sort_by
-
@map.sort_by {|k, v| yield k, v}
-
end
-
-
1
def update(map)
-
map = map.as_stored if map.is_a?(NormalizedMap)
-
map.each {|k, v| self[k] = v}
-
end
-
-
1
def method_missing(method, *args, &block)
-
@map.send(method, *args, &block)
-
end
-
-
1
def respond_to_missing?(method, include_private = false)
-
@map.respond_to?(method, include_private)
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
module SassC
-
1
VERSION = "2.4.0"
-
end
-
# coding: utf-8
-
# frozen_string_literal: true
-
-
# external dependencies
-
1
require 'rack'
-
1
require 'tilt'
-
1
require 'rack/protection'
-
1
require 'mustermann'
-
1
require 'mustermann/sinatra'
-
1
require 'mustermann/regular'
-
-
# stdlib dependencies
-
1
require 'thread'
-
1
require 'time'
-
1
require 'uri'
-
-
# other files we need
-
1
require 'sinatra/indifferent_hash'
-
1
require 'sinatra/show_exceptions'
-
1
require 'sinatra/version'
-
-
1
module Sinatra
-
# The request object. See Rack::Request for more info:
-
# http://rubydoc.info/github/rack/rack/master/Rack/Request
-
1
class Request < Rack::Request
-
1
HEADER_PARAM = /\s*[\w.]+=(?:[\w.]+|"(?:[^"\\]|\\.)*")?\s*/
-
1
HEADER_VALUE_WITH_PARAMS = /(?:(?:\w+|\*)\/(?:\w+(?:\.|\-|\+)?|\*)*)\s*(?:;#{HEADER_PARAM})*/
-
-
# Returns an array of acceptable media types for the response
-
1
def accept
-
2
@env['sinatra.accept'] ||= begin
-
2
if @env.include? 'HTTP_ACCEPT' and @env['HTTP_ACCEPT'].to_s != ''
-
@env['HTTP_ACCEPT'].to_s.scan(HEADER_VALUE_WITH_PARAMS).
-
map! { |e| AcceptEntry.new(e) }.sort
-
else
-
2
[AcceptEntry.new('*/*')]
-
end
-
end
-
end
-
-
1
def accept?(type)
-
preferred_type(type).to_s.include?(type)
-
end
-
-
1
def preferred_type(*types)
-
2
accepts = accept # just evaluate once
-
2
return accepts.first if types.empty?
-
2
types.flatten!
-
2
return types.first if accepts.empty?
-
2
accepts.detect do |pattern|
-
4
type = types.detect { |t| File.fnmatch(pattern, t) }
-
2
return type if type
-
end
-
end
-
-
1
alias secure? ssl?
-
-
1
def forwarded?
-
@env.include? "HTTP_X_FORWARDED_HOST"
-
end
-
-
1
def safe?
-
get? or head? or options? or trace?
-
end
-
-
1
def idempotent?
-
safe? or put? or delete? or link? or unlink?
-
end
-
-
1
def link?
-
request_method == "LINK"
-
end
-
-
1
def unlink?
-
request_method == "UNLINK"
-
end
-
-
1
def params
-
2
super
-
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
-
raise BadRequest, "Invalid query parameters: #{Rack::Utils.escape_html(e.message)}"
-
end
-
-
1
private
-
-
1
class AcceptEntry
-
1
attr_accessor :params
-
1
attr_reader :entry
-
-
1
def initialize(entry)
-
2
params = entry.scan(HEADER_PARAM).map! do |s|
-
key, value = s.strip.split('=', 2)
-
value = value[1..-2].gsub(/\\(.)/, '\1') if value.start_with?('"')
-
[key, value]
-
end
-
-
2
@entry = entry
-
2
@type = entry[/[^;]+/].delete(' ')
-
2
@params = Hash[params]
-
4
@q = @params.delete('q') { 1.0 }.to_f
-
end
-
-
1
def <=>(other)
-
other.priority <=> self.priority
-
end
-
-
1
def priority
-
# We sort in descending order; better matches should be higher.
-
[ @q, -@type.count('*'), @params.size ]
-
end
-
-
1
def to_str
-
2
@type
-
end
-
-
1
def to_s(full = false)
-
full ? entry : to_str
-
end
-
-
1
def respond_to?(*args)
-
2
super or to_str.respond_to?(*args)
-
end
-
-
1
def method_missing(*args, &block)
-
to_str.send(*args, &block)
-
end
-
end
-
end
-
-
# The response object. See Rack::Response and Rack::Response::Helpers for
-
# more info:
-
# http://rubydoc.info/github/rack/rack/master/Rack/Response
-
# http://rubydoc.info/github/rack/rack/master/Rack/Response/Helpers
-
1
class Response < Rack::Response
-
1
DROP_BODY_RESPONSES = [204, 304]
-
1
def initialize(*)
-
2
super
-
2
headers['Content-Type'] ||= 'text/html'
-
end
-
-
1
def body=(value)
-
2
value = value.body while Rack::Response === value
-
2
@body = String === value ? [value.to_str] : value
-
end
-
-
1
def each
-
block_given? ? super : enum_for(:each)
-
end
-
-
1
def finish
-
2
result = body
-
-
2
if drop_content_info?
-
headers.delete "Content-Length"
-
headers.delete "Content-Type"
-
end
-
-
2
if drop_body?
-
close
-
result = []
-
end
-
-
2
if calculate_content_length?
-
# if some other code has already set Content-Length, don't muck with it
-
# currently, this would be the static file-handler
-
headers["Content-Length"] = body.inject(0) { |l, p| l + p.bytesize }.to_s
-
end
-
-
2
[status.to_i, headers, result]
-
end
-
-
1
private
-
-
1
def calculate_content_length?
-
2
headers["Content-Type"] and not headers["Content-Length"] and Array === body
-
end
-
-
1
def drop_content_info?
-
2
status.to_i / 100 == 1 or drop_body?
-
end
-
-
1
def drop_body?
-
4
DROP_BODY_RESPONSES.include?(status.to_i)
-
end
-
end
-
-
# Some Rack handlers (Thin, Rainbows!) implement an extended body object protocol, however,
-
# some middleware (namely Rack::Lint) will break it by not mirroring the methods in question.
-
# This middleware will detect an extended body object and will make sure it reaches the
-
# handler directly. We do this here, so our middleware and middleware set up by the app will
-
# still be able to run.
-
1
class ExtendedRack < Struct.new(:app)
-
1
def call(env)
-
2
result, callback = app.call(env), env['async.callback']
-
2
return result unless callback and async?(*result)
-
after_response { callback.call result }
-
setup_close(env, *result)
-
throw :async
-
end
-
-
1
private
-
-
1
def setup_close(env, status, headers, body)
-
return unless body.respond_to? :close and env.include? 'async.close'
-
env['async.close'].callback { body.close }
-
env['async.close'].errback { body.close }
-
end
-
-
1
def after_response(&block)
-
raise NotImplementedError, "only supports EventMachine at the moment" unless defined? EventMachine
-
EventMachine.next_tick(&block)
-
end
-
-
1
def async?(status, headers, body)
-
return true if status == -1
-
body.respond_to? :callback and body.respond_to? :errback
-
end
-
end
-
-
# Behaves exactly like Rack::CommonLogger with the notable exception that it does nothing,
-
# if another CommonLogger is already in the middleware chain.
-
1
class CommonLogger < Rack::CommonLogger
-
1
def call(env)
-
env['sinatra.commonlogger'] ? @app.call(env) : super
-
end
-
-
1
superclass.class_eval do
-
1
alias call_without_check call unless method_defined? :call_without_check
-
1
def call(env)
-
env['sinatra.commonlogger'] = true
-
call_without_check(env)
-
end
-
end
-
end
-
-
1
class BadRequest < TypeError #:nodoc:
-
1
def http_status; 400 end
-
end
-
-
1
class NotFound < NameError #:nodoc:
-
1
def http_status; 404 end
-
end
-
-
# Methods available to routes, before/after filters, and views.
-
1
module Helpers
-
# Set or retrieve the response status code.
-
1
def status(value = nil)
-
2
response.status = Rack::Utils.status_code(value) if value
-
2
response.status
-
end
-
-
# Set or retrieve the response body. When a block is given,
-
# evaluation is deferred until the body is read with #each.
-
1
def body(value = nil, &block)
-
2
if block_given?
-
def block.each; yield(call) end
-
response.body = block
-
2
elsif value
-
# Rack 2.0 returns a Rack::File::Iterator here instead of
-
# Rack::File as it was in the previous API.
-
2
unless request.head? || value.is_a?(Rack::File::Iterator) || value.is_a?(Stream)
-
2
headers.delete 'Content-Length'
-
end
-
2
response.body = value
-
else
-
response.body
-
end
-
end
-
-
# Halt processing and redirect to the URI provided.
-
1
def redirect(uri, *args)
-
if env['HTTP_VERSION'] == 'HTTP/1.1' and env["REQUEST_METHOD"] != 'GET'
-
status 303
-
else
-
status 302
-
end
-
-
# According to RFC 2616 section 14.30, "the field value consists of a
-
# single absolute URI"
-
response['Location'] = uri(uri.to_s, settings.absolute_redirects?, settings.prefixed_redirects?)
-
halt(*args)
-
end
-
-
# Generates the absolute URI for a given path in the app.
-
# Takes Rack routers and reverse proxies into account.
-
1
def uri(addr = nil, absolute = true, add_script_name = true)
-
return addr if addr =~ /\A[a-z][a-z0-9\+\.\-]*:/i
-
uri = [host = String.new]
-
if absolute
-
host << "http#{'s' if request.secure?}://"
-
if request.forwarded? or request.port != (request.secure? ? 443 : 80)
-
host << request.host_with_port
-
else
-
host << request.host
-
end
-
end
-
uri << request.script_name.to_s if add_script_name
-
uri << (addr ? addr : request.path_info).to_s
-
File.join uri
-
end
-
-
1
alias url uri
-
1
alias to uri
-
-
# Halt processing and return the error status provided.
-
1
def error(code, body = nil)
-
code, body = 500, code.to_str if code.respond_to? :to_str
-
response.body = body unless body.nil?
-
halt code
-
end
-
-
# Halt processing and return a 404 Not Found.
-
1
def not_found(body = nil)
-
error 404, body
-
end
-
-
# Set multiple response headers with Hash.
-
1
def headers(hash = nil)
-
4
response.headers.merge! hash if hash
-
4
response.headers
-
end
-
-
# Access the underlying Rack session.
-
1
def session
-
request.session
-
end
-
-
# Access shared logger object.
-
1
def logger
-
request.logger
-
end
-
-
# Look up a media type by file extension in Rack's mime registry.
-
1
def mime_type(type)
-
6
Base.mime_type(type)
-
end
-
-
# Set the Content-Type of the response body given a media type or file
-
# extension.
-
1
def content_type(type = nil, params = {})
-
6
return response['Content-Type'] unless type
-
4
default = params.delete :default
-
4
mime_type = mime_type(type) || default
-
4
fail "Unknown media type: %p" % type if mime_type.nil?
-
4
mime_type = mime_type.dup
-
17
unless params.include? :charset or settings.add_charset.all? { |p| not p === mime_type }
-
3
params[:charset] = params.delete('charset') || settings.default_encoding
-
end
-
4
params.delete :charset if mime_type.include? 'charset'
-
4
unless params.empty?
-
2
mime_type << (mime_type.include?(';') ? ', ' : ';')
-
2
mime_type << params.map do |key, val|
-
2
val = val.inspect if val =~ /[";,]/
-
2
"#{key}=#{val}"
-
end.join(', ')
-
end
-
4
response['Content-Type'] = mime_type
-
end
-
-
# Set the Content-Disposition to "attachment" with the specified filename,
-
# instructing the user agents to prompt to save.
-
1
def attachment(filename = nil, disposition = :attachment)
-
response['Content-Disposition'] = disposition.to_s.dup
-
if filename
-
params = '; filename="%s"' % File.basename(filename)
-
response['Content-Disposition'] << params
-
ext = File.extname(filename)
-
content_type(ext) unless response['Content-Type'] or ext.empty?
-
end
-
end
-
-
# Use the contents of the file at +path+ as the response body.
-
1
def send_file(path, opts = {})
-
if opts[:type] or not response['Content-Type']
-
content_type opts[:type] || File.extname(path), :default => 'application/octet-stream'
-
end
-
-
disposition = opts[:disposition]
-
filename = opts[:filename]
-
disposition = :attachment if disposition.nil? and filename
-
filename = path if filename.nil?
-
attachment(filename, disposition) if disposition
-
-
last_modified opts[:last_modified] if opts[:last_modified]
-
-
file = Rack::File.new(File.dirname(settings.app_file))
-
result = file.serving(request, path)
-
-
result[1].each { |k,v| headers[k] ||= v }
-
headers['Content-Length'] = result[1]['Content-Length']
-
opts[:status] &&= Integer(opts[:status])
-
halt (opts[:status] || result[0]), result[2]
-
rescue Errno::ENOENT
-
not_found
-
end
-
-
# Class of the response body in case you use #stream.
-
#
-
# Three things really matter: The front and back block (back being the
-
# block generating content, front the one sending it to the client) and
-
# the scheduler, integrating with whatever concurrency feature the Rack
-
# handler is using.
-
#
-
# Scheduler has to respond to defer and schedule.
-
1
class Stream
-
1
def self.schedule(*) yield end
-
1
def self.defer(*) yield end
-
-
1
def initialize(scheduler = self.class, keep_open = false, &back)
-
@back, @scheduler, @keep_open = back.to_proc, scheduler, keep_open
-
@callbacks, @closed = [], false
-
end
-
-
1
def close
-
return if closed?
-
@closed = true
-
@scheduler.schedule { @callbacks.each { |c| c.call } }
-
end
-
-
1
def each(&front)
-
@front = front
-
@scheduler.defer do
-
begin
-
@back.call(self)
-
rescue Exception => e
-
@scheduler.schedule { raise e }
-
end
-
close unless @keep_open
-
end
-
end
-
-
1
def <<(data)
-
@scheduler.schedule { @front.call(data.to_s) }
-
self
-
end
-
-
1
def callback(&block)
-
return yield if closed?
-
@callbacks << block
-
end
-
-
1
alias errback callback
-
-
1
def closed?
-
@closed
-
end
-
end
-
-
# Allows to start sending data to the client even though later parts of
-
# the response body have not yet been generated.
-
#
-
# The close parameter specifies whether Stream#close should be called
-
# after the block has been executed. This is only relevant for evented
-
# servers like Thin or Rainbows.
-
1
def stream(keep_open = false)
-
scheduler = env['async.callback'] ? EventMachine : Stream
-
current = @params.dup
-
body Stream.new(scheduler, keep_open) { |out| with_params(current) { yield(out) } }
-
end
-
-
# Specify response freshness policy for HTTP caches (Cache-Control header).
-
# Any number of non-value directives (:public, :private, :no_cache,
-
# :no_store, :must_revalidate, :proxy_revalidate) may be passed along with
-
# a Hash of value directives (:max_age, :s_maxage).
-
#
-
# cache_control :public, :must_revalidate, :max_age => 60
-
# => Cache-Control: public, must-revalidate, max-age=60
-
#
-
# See RFC 2616 / 14.9 for more on standard cache control directives:
-
# http://tools.ietf.org/html/rfc2616#section-14.9.1
-
1
def cache_control(*values)
-
if values.last.kind_of?(Hash)
-
hash = values.pop
-
hash.reject! { |k, v| v == false }
-
hash.reject! { |k, v| values << k if v == true }
-
else
-
hash = {}
-
end
-
-
values.map! { |value| value.to_s.tr('_','-') }
-
hash.each do |key, value|
-
key = key.to_s.tr('_', '-')
-
value = value.to_i if ['max-age', 's-maxage'].include? key
-
values << "#{key}=#{value}"
-
end
-
-
response['Cache-Control'] = values.join(', ') if values.any?
-
end
-
-
# Set the Expires header and Cache-Control/max-age directive. Amount
-
# can be an integer number of seconds in the future or a Time object
-
# indicating when the response should be considered "stale". The remaining
-
# "values" arguments are passed to the #cache_control helper:
-
#
-
# expires 500, :public, :must_revalidate
-
# => Cache-Control: public, must-revalidate, max-age=500
-
# => Expires: Mon, 08 Jun 2009 08:50:17 GMT
-
#
-
1
def expires(amount, *values)
-
values << {} unless values.last.kind_of?(Hash)
-
-
if amount.is_a? Integer
-
time = Time.now + amount.to_i
-
max_age = amount
-
else
-
time = time_for amount
-
max_age = time - Time.now
-
end
-
-
values.last.merge!(:max_age => max_age)
-
cache_control(*values)
-
-
response['Expires'] = time.httpdate
-
end
-
-
# Set the last modified time of the resource (HTTP 'Last-Modified' header)
-
# and halt if conditional GET matches. The +time+ argument is a Time,
-
# DateTime, or other object that responds to +to_time+.
-
#
-
# When the current request includes an 'If-Modified-Since' header that is
-
# equal or later than the time specified, execution is immediately halted
-
# with a '304 Not Modified' response.
-
1
def last_modified(time)
-
return unless time
-
time = time_for time
-
response['Last-Modified'] = time.httpdate
-
return if env['HTTP_IF_NONE_MATCH']
-
-
if status == 200 and env['HTTP_IF_MODIFIED_SINCE']
-
# compare based on seconds since epoch
-
since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i
-
halt 304 if since >= time.to_i
-
end
-
-
if (success? or status == 412) and env['HTTP_IF_UNMODIFIED_SINCE']
-
# compare based on seconds since epoch
-
since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i
-
halt 412 if since < time.to_i
-
end
-
rescue ArgumentError
-
end
-
-
1
ETAG_KINDS = [:strong, :weak]
-
# Set the response entity tag (HTTP 'ETag' header) and halt if conditional
-
# GET matches. The +value+ argument is an identifier that uniquely
-
# identifies the current version of the resource. The +kind+ argument
-
# indicates whether the etag should be used as a :strong (default) or :weak
-
# cache validator.
-
#
-
# When the current request includes an 'If-None-Match' header with a
-
# matching etag, execution is immediately halted. If the request method is
-
# GET or HEAD, a '304 Not Modified' response is sent.
-
1
def etag(value, options = {})
-
# Before touching this code, please double check RFC 2616 14.24 and 14.26.
-
options = {:kind => options} unless Hash === options
-
kind = options[:kind] || :strong
-
new_resource = options.fetch(:new_resource) { request.post? }
-
-
unless ETAG_KINDS.include?(kind)
-
raise ArgumentError, ":strong or :weak expected"
-
end
-
-
value = '"%s"' % value
-
value = "W/#{value}" if kind == :weak
-
response['ETag'] = value
-
-
if success? or status == 304
-
if etag_matches? env['HTTP_IF_NONE_MATCH'], new_resource
-
halt(request.safe? ? 304 : 412)
-
end
-
-
if env['HTTP_IF_MATCH']
-
halt 412 unless etag_matches? env['HTTP_IF_MATCH'], new_resource
-
end
-
end
-
end
-
-
# Sugar for redirect (example: redirect back)
-
1
def back
-
request.referer
-
end
-
-
# whether or not the status is set to 1xx
-
1
def informational?
-
status.between? 100, 199
-
end
-
-
# whether or not the status is set to 2xx
-
1
def success?
-
status.between? 200, 299
-
end
-
-
# whether or not the status is set to 3xx
-
1
def redirect?
-
status.between? 300, 399
-
end
-
-
# whether or not the status is set to 4xx
-
1
def client_error?
-
status.between? 400, 499
-
end
-
-
# whether or not the status is set to 5xx
-
1
def server_error?
-
status.between? 500, 599
-
end
-
-
# whether or not the status is set to 404
-
1
def not_found?
-
status == 404
-
end
-
-
# whether or not the status is set to 400
-
1
def bad_request?
-
status == 400
-
end
-
-
# Generates a Time object from the given value.
-
# Used by #expires and #last_modified.
-
1
def time_for(value)
-
if value.is_a? Numeric
-
Time.at value
-
elsif value.respond_to? :to_s
-
Time.parse value.to_s
-
else
-
value.to_time
-
end
-
rescue ArgumentError => boom
-
raise boom
-
rescue Exception
-
raise ArgumentError, "unable to convert #{value.inspect} to a Time object"
-
end
-
-
1
private
-
-
# Helper method checking if a ETag value list includes the current ETag.
-
1
def etag_matches?(list, new_resource = request.post?)
-
return !new_resource if list == '*'
-
list.to_s.split(/\s*,\s*/).include? response['ETag']
-
end
-
-
1
def with_params(temp_params)
-
original, @params = @params, temp_params
-
yield
-
ensure
-
@params = original if original
-
end
-
end
-
-
1
private
-
-
# Template rendering methods. Each method takes the name of a template
-
# to render as a Symbol and returns a String with the rendered output,
-
# as well as an optional hash with additional options.
-
#
-
# `template` is either the name or path of the template as symbol
-
# (Use `:'subdir/myview'` for views in subdirectories), or a string
-
# that will be rendered.
-
#
-
# Possible options are:
-
# :content_type The content type to use, same arguments as content_type.
-
# :layout If set to something falsy, no layout is rendered, otherwise
-
# the specified layout is used (Ignored for `sass` and `less`)
-
# :layout_engine Engine to use for rendering the layout.
-
# :locals A hash with local variables that should be available
-
# in the template
-
# :scope If set, template is evaluate with the binding of the given
-
# object rather than the application instance.
-
# :views Views directory to use.
-
1
module Templates
-
1
module ContentTyped
-
1
attr_accessor :content_type
-
end
-
-
1
def initialize
-
2
super
-
2
@default_layout = :layout
-
2
@preferred_extension = nil
-
end
-
-
1
def erb(template, options = {}, locals = {}, &block)
-
render(:erb, template, options, locals, &block)
-
end
-
-
1
def erubis(template, options = {}, locals = {})
-
warn "Sinatra::Templates#erubis is deprecated and will be removed, use #erb instead.\n" \
-
"If you have Erubis installed, it will be used automatically."
-
render :erubis, template, options, locals
-
end
-
-
1
def haml(template, options = {}, locals = {}, &block)
-
render(:haml, template, options, locals, &block)
-
end
-
-
1
def sass(template, options = {}, locals = {})
-
options.merge! :layout => false, :default_content_type => :css
-
render :sass, template, options, locals
-
end
-
-
1
def scss(template, options = {}, locals = {})
-
options.merge! :layout => false, :default_content_type => :css
-
render :scss, template, options, locals
-
end
-
-
1
def less(template, options = {}, locals = {})
-
options.merge! :layout => false, :default_content_type => :css
-
render :less, template, options, locals
-
end
-
-
1
def stylus(template, options = {}, locals = {})
-
options.merge! :layout => false, :default_content_type => :css
-
render :styl, template, options, locals
-
end
-
-
1
def builder(template = nil, options = {}, locals = {}, &block)
-
options[:default_content_type] = :xml
-
render_ruby(:builder, template, options, locals, &block)
-
end
-
-
1
def liquid(template, options = {}, locals = {}, &block)
-
render(:liquid, template, options, locals, &block)
-
end
-
-
1
def markdown(template, options = {}, locals = {})
-
options[:exclude_outvar] = true
-
render :markdown, template, options, locals
-
end
-
-
1
def textile(template, options = {}, locals = {})
-
render :textile, template, options, locals
-
end
-
-
1
def rdoc(template, options = {}, locals = {})
-
render :rdoc, template, options, locals
-
end
-
-
1
def asciidoc(template, options = {}, locals = {})
-
render :asciidoc, template, options, locals
-
end
-
-
1
def radius(template, options = {}, locals = {})
-
render :radius, template, options, locals
-
end
-
-
1
def markaby(template = nil, options = {}, locals = {}, &block)
-
render_ruby(:mab, template, options, locals, &block)
-
end
-
-
1
def coffee(template, options = {}, locals = {})
-
options.merge! :layout => false, :default_content_type => :js
-
render :coffee, template, options, locals
-
end
-
-
1
def nokogiri(template = nil, options = {}, locals = {}, &block)
-
options[:default_content_type] = :xml
-
render_ruby(:nokogiri, template, options, locals, &block)
-
end
-
-
1
def slim(template, options = {}, locals = {}, &block)
-
render(:slim, template, options, locals, &block)
-
end
-
-
1
def creole(template, options = {}, locals = {})
-
render :creole, template, options, locals
-
end
-
-
1
def mediawiki(template, options = {}, locals = {})
-
render :mediawiki, template, options, locals
-
end
-
-
1
def wlang(template, options = {}, locals = {}, &block)
-
render(:wlang, template, options, locals, &block)
-
end
-
-
1
def yajl(template, options = {}, locals = {})
-
options[:default_content_type] = :json
-
render :yajl, template, options, locals
-
end
-
-
1
def rabl(template, options = {}, locals = {})
-
Rabl.register!
-
render :rabl, template, options, locals
-
end
-
-
# Calls the given block for every possible template file in views,
-
# named name.ext, where ext is registered on engine.
-
1
def find_template(views, name, engine)
-
yield ::File.join(views, "#{name}.#{@preferred_extension}")
-
-
Tilt.default_mapping.extensions_for(engine).each do |ext|
-
yield ::File.join(views, "#{name}.#{ext}") unless ext == @preferred_extension
-
end
-
end
-
-
1
private
-
-
# logic shared between builder and nokogiri
-
1
def render_ruby(engine, template, options = {}, locals = {}, &block)
-
options, template = template, nil if template.is_a?(Hash)
-
template = Proc.new { block } if template.nil?
-
render engine, template, options, locals
-
end
-
-
1
def render(engine, data, options = {}, locals = {}, &block)
-
# merge app-level options
-
engine_options = settings.respond_to?(engine) ? settings.send(engine) : {}
-
options.merge!(engine_options) { |key, v1, v2| v1 }
-
-
# extract generic options
-
locals = options.delete(:locals) || locals || {}
-
views = options.delete(:views) || settings.views || "./views"
-
layout = options[:layout]
-
layout = false if layout.nil? && options.include?(:layout)
-
eat_errors = layout.nil?
-
layout = engine_options[:layout] if layout.nil? or (layout == true && engine_options[:layout] != false)
-
layout = @default_layout if layout.nil? or layout == true
-
layout_options = options.delete(:layout_options) || {}
-
content_type = options.delete(:default_content_type)
-
content_type = options.delete(:content_type) || content_type
-
layout_engine = options.delete(:layout_engine) || engine
-
scope = options.delete(:scope) || self
-
exclude_outvar = options.delete(:exclude_outvar)
-
options.delete(:layout)
-
-
# set some defaults
-
options[:outvar] ||= '@_out_buf' unless exclude_outvar
-
options[:default_encoding] ||= settings.default_encoding
-
-
# compile and render template
-
begin
-
layout_was = @default_layout
-
@default_layout = false
-
template = compile_template(engine, data, options, views)
-
output = template.render(scope, locals, &block)
-
ensure
-
@default_layout = layout_was
-
end
-
-
# render layout
-
if layout
-
options = options.merge(:views => views, :layout => false, :eat_errors => eat_errors, :scope => scope).
-
merge!(layout_options)
-
catch(:layout_missing) { return render(layout_engine, layout, options, locals) { output } }
-
end
-
-
output.extend(ContentTyped).content_type = content_type if content_type
-
output
-
end
-
-
1
def compile_template(engine, data, options, views)
-
eat_errors = options.delete :eat_errors
-
template_cache.fetch engine, data, options, views do
-
template = Tilt[engine]
-
raise "Template engine not found: #{engine}" if template.nil?
-
-
case data
-
when Symbol
-
body, path, line = settings.templates[data]
-
if body
-
body = body.call if body.respond_to?(:call)
-
template.new(path, line.to_i, options) { body }
-
else
-
found = false
-
@preferred_extension = engine.to_s
-
find_template(views, data, template) do |file|
-
path ||= file # keep the initial path rather than the last one
-
if found = File.exist?(file)
-
path = file
-
break
-
end
-
end
-
throw :layout_missing if eat_errors and not found
-
template.new(path, 1, options)
-
end
-
when Proc, String
-
body = data.is_a?(String) ? Proc.new { data } : data
-
caller = settings.caller_locations.first
-
path = options[:path] || caller[0]
-
line = options[:line] || caller[1]
-
template.new(path, line.to_i, options, &body)
-
else
-
raise ArgumentError, "Sorry, don't know how to render #{data.inspect}."
-
end
-
end
-
end
-
end
-
-
# Base class for all Sinatra applications and middleware.
-
1
class Base
-
1
include Rack::Utils
-
1
include Helpers
-
1
include Templates
-
-
1
URI_INSTANCE = URI::Parser.new
-
-
1
attr_accessor :app, :env, :request, :response, :params
-
1
attr_reader :template_cache
-
-
1
def initialize(app = nil)
-
2
super()
-
2
@app = app
-
2
@template_cache = Tilt::Cache.new
-
2
yield self if block_given?
-
end
-
-
# Rack call interface.
-
1
def call(env)
-
2
dup.call!(env)
-
end
-
-
1
def call!(env) # :nodoc:
-
2
@env = env
-
2
@params = IndifferentHash.new
-
2
@request = Request.new(env)
-
2
@response = Response.new
-
2
template_cache.clear if settings.reload_templates
-
-
2
@response['Content-Type'] = nil
-
4
invoke { dispatch! }
-
4
invoke { error_block!(response.status) } unless @env['sinatra.error']
-
-
2
unless @response['Content-Type']
-
if Array === body and body[0].respond_to? :content_type
-
content_type body[0].content_type
-
else
-
content_type :html
-
end
-
end
-
-
2
@response.finish
-
end
-
-
# Access settings defined with Base.set.
-
1
def self.settings
-
51
self
-
end
-
-
# Access settings defined with Base.set.
-
1
def settings
-
44
self.class.settings
-
end
-
-
1
def options
-
warn "Sinatra::Base#options is deprecated and will be removed, " \
-
"use #settings instead."
-
settings
-
end
-
-
# Exit the current block, halts any further processing
-
# of the request, and returns the specified response.
-
1
def halt(*response)
-
2
response = response.first if response.length == 1
-
2
throw :halt, response
-
end
-
-
# Pass control to the next matching route.
-
# If there are no more matching routes, Sinatra will
-
# return a 404 response.
-
1
def pass(&block)
-
throw :pass, block
-
end
-
-
# Forward the request to the downstream app -- middleware only.
-
1
def forward
-
fail "downstream app not set" unless @app.respond_to? :call
-
status, headers, body = @app.call env
-
@response.status = status
-
@response.body = body
-
@response.headers.merge! headers
-
nil
-
end
-
-
1
private
-
-
# Run filters defined on the class and all superclasses.
-
1
def filter!(type, base = settings)
-
12
filter! type, base.superclass if base.superclass.respond_to?(:filters)
-
16
base.filters[type].each { |args| process_route(*args) }
-
end
-
-
# Run routes defined on the class and all superclasses.
-
1
def route!(base = settings, pass_block = nil)
-
4
if routes = base.routes[@request.request_method]
-
4
routes.each do |pattern, conditions, block|
-
11
returned_pass_block = process_route(pattern, conditions) do |*args|
-
2
env['sinatra.route'] = "#{@request.request_method} #{pattern}"
-
4
route_eval { block[*args] }
-
end
-
-
# don't wipe out pass_block in superclass
-
9
pass_block = returned_pass_block if returned_pass_block
-
end
-
end
-
-
# Run routes defined in superclass.
-
2
if base.superclass.respond_to?(:routes)
-
2
return route!(base.superclass, pass_block)
-
end
-
-
route_eval(&pass_block) if pass_block
-
route_missing
-
end
-
-
# Run a route block and throw :halt with the result.
-
1
def route_eval
-
2
throw :halt, yield
-
end
-
-
# If the current request matches pattern and conditions, fill params
-
# with keys and call the given block.
-
# Revert params afterwards.
-
#
-
# Returns pass block.
-
1
def process_route(pattern, conditions, block = nil, values = [])
-
15
route = @request.path_info
-
15
route = '/' if route.empty? and not settings.empty_path_info?
-
15
route = route[0..-2] if !settings.strict_paths? && route != '/' && route.end_with?('/')
-
15
return unless params = pattern.params(route)
-
-
6
params.delete("ignore") # TODO: better params handling, maybe turn it into "smart" object or detect changes
-
6
force_encoding(params)
-
6
@params = @params.merge(params) if params.any?
-
-
6
regexp_exists = pattern.is_a?(Mustermann::Regular) || (pattern.respond_to?(:patterns) && pattern.patterns.any? {|subpattern| subpattern.is_a?(Mustermann::Regular)} )
-
6
if regexp_exists
-
4
captures = pattern.match(route).captures.map { |c| URI_INSTANCE.unescape(c) if c }
-
4
values += captures
-
4
@params[:captures] = force_encoding(captures) unless captures.nil? || captures.empty?
-
else
-
2
values += params.values.flatten
-
end
-
-
6
catch(:pass) do
-
8
conditions.each { |c| throw :pass if c.bind(self).call == false }
-
6
block ? block[self, values] : yield(self, values)
-
end
-
rescue
-
@env['sinatra.error.params'] = @params
-
raise
-
ensure
-
15
params ||= {}
-
15
params.each { |k, _| @params.delete(k) } unless @env['sinatra.error.params']
-
end
-
-
# No matching route was found or all routes passed. The default
-
# implementation is to forward the request downstream when running
-
# as middleware (@app is non-nil); when no downstream app is set, raise
-
# a NotFound exception. Subclasses can override this method to perform
-
# custom route miss logic.
-
1
def route_missing
-
if @app
-
forward
-
else
-
raise NotFound, "#{request.request_method} #{request.path_info}"
-
end
-
end
-
-
# Attempt to serve static files from public directory. Throws :halt when
-
# a matching file is found, returns nil otherwise.
-
1
def static!(options = {})
-
return if (public_dir = settings.public_folder).nil?
-
path = File.expand_path("#{public_dir}#{URI_INSTANCE.unescape(request.path_info)}" )
-
return unless File.file?(path)
-
-
env['sinatra.static_file'] = path
-
cache_control(*settings.static_cache_control) if settings.static_cache_control?
-
send_file path, options.merge(:disposition => nil)
-
end
-
-
# Run the block with 'throw :halt' support and apply result to the response.
-
1
def invoke
-
12
res = catch(:halt) { yield }
-
-
6
res = [res] if Integer === res or String === res
-
6
if Array === res and Integer === res.first
-
2
res = res.dup
-
2
status(res.shift)
-
2
body(res.pop)
-
2
headers(*res)
-
4
elsif res.respond_to? :each
-
body res
-
end
-
nil # avoid double setting the same response tuple twice
-
end
-
-
# Dispatch a request with error handling.
-
1
def dispatch!
-
# Avoid passing frozen string in force_encoding
-
2
@params.merge!(@request.params).each do |key, val|
-
next unless val.respond_to?(:force_encoding)
-
val = val.dup if val.frozen?
-
@params[key] = force_encoding(val)
-
end
-
-
2
invoke do
-
2
static! if settings.static? && (request.get? || request.head?)
-
2
filter! :before
-
2
route!
-
end
-
rescue ::Exception => boom
-
invoke { handle_exception!(boom) }
-
ensure
-
begin
-
2
filter! :after unless env['sinatra.static_file']
-
rescue ::Exception => boom
-
invoke { handle_exception!(boom) } unless @env['sinatra.error']
-
end
-
end
-
-
# Error handling during requests.
-
1
def handle_exception!(boom)
-
if error_params = @env['sinatra.error.params']
-
@params = @params.merge(error_params)
-
end
-
@env['sinatra.error'] = boom
-
-
if boom.respond_to? :http_status
-
status(boom.http_status)
-
elsif settings.use_code? and boom.respond_to? :code and boom.code.between? 400, 599
-
status(boom.code)
-
else
-
status(500)
-
end
-
-
status(500) unless status.between? 400, 599
-
-
boom_message = boom.message if boom.message && boom.message != boom.class.name
-
if server_error?
-
dump_errors! boom if settings.dump_errors?
-
raise boom if settings.show_exceptions? and settings.show_exceptions != :after_handler
-
elsif not_found?
-
headers['X-Cascade'] = 'pass' if settings.x_cascade?
-
body boom_message || '<h1>Not Found</h1>'
-
elsif bad_request?
-
body boom_message || '<h1>Bad Request</h1>'
-
end
-
-
res = error_block!(boom.class, boom) || error_block!(status, boom)
-
return res if res or not server_error?
-
raise boom if settings.raise_errors? or settings.show_exceptions?
-
error_block! Exception, boom
-
end
-
-
# Find an custom error block for the key(s) specified.
-
1
def error_block!(key, *block_params)
-
2
base = settings
-
2
while base.respond_to?(:errors)
-
6
next base = base.superclass unless args_array = base.errors[key]
-
args_array.reverse_each do |args|
-
first = args == args_array.first
-
args += [block_params]
-
resp = process_route(*args)
-
return resp unless resp.nil? && !first
-
end
-
end
-
2
return false unless key.respond_to? :superclass and key.superclass < Exception
-
error_block!(key.superclass, *block_params)
-
end
-
-
1
def dump_errors!(boom)
-
msg = ["#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} - #{boom.class} - #{boom.message}:", *boom.backtrace].join("\n\t")
-
@env['rack.errors'].puts(msg)
-
end
-
-
1
class << self
-
1
CALLERS_TO_IGNORE = [ # :nodoc:
-
/\/sinatra(\/(base|main|show_exceptions))?\.rb$/, # all sinatra code
-
/lib\/tilt.*\.rb$/, # all tilt code
-
/^\(.*\)$/, # generated code
-
/rubygems\/(custom|core_ext\/kernel)_require\.rb$/, # rubygems require hacks
-
/active_support/, # active_support require hacks
-
/bundler(\/(?:runtime|inline))?\.rb/, # bundler require hacks
-
/<internal:/, # internal in ruby >= 1.9.2
-
/src\/kernel\/bootstrap\/[A-Z]/ # maglev kernel files
-
]
-
-
# contrary to what the comment said previously, rubinius never supported this
-
1
if defined?(RUBY_IGNORE_CALLERS)
-
warn "RUBY_IGNORE_CALLERS is deprecated and will no longer be supported by Sinatra 2.0"
-
CALLERS_TO_IGNORE.concat(RUBY_IGNORE_CALLERS)
-
end
-
-
1
attr_reader :routes, :filters, :templates, :errors
-
-
# Removes all routes, filters, middleware and extension hooks from the
-
# current class (not routes/filters/... defined by its superclass).
-
1
def reset!
-
4
@conditions = []
-
4
@routes = {}
-
4
@filters = {:before => [], :after => []}
-
4
@errors = {}
-
4
@middleware = []
-
4
@prototype = nil
-
4
@extensions = []
-
-
4
if superclass.respond_to?(:templates)
-
3
@templates = Hash.new { |hash, key| superclass.templates[key] }
-
else
-
1
@templates = {}
-
end
-
end
-
-
# Extension modules registered on this class and all superclasses.
-
1
def extensions
-
34
if superclass.respond_to?(:extensions)
-
20
(@extensions + superclass.extensions).uniq
-
else
-
14
@extensions
-
end
-
end
-
-
# Middleware used in this class and all superclasses.
-
1
def middleware
-
6
if superclass.respond_to?(:middleware)
-
4
superclass.middleware + @middleware
-
else
-
2
@middleware
-
end
-
end
-
-
# Sets an option to the given value. If the value is a proc,
-
# the proc will be called every time the option is accessed.
-
8
def set(option, value = (not_set = true), ignore_setter = false, &block)
-
73
raise ArgumentError if block and !not_set
-
73
value, not_set = block, false if block
-
-
73
if not_set
-
raise ArgumentError unless option.respond_to?(:each)
-
option.each { |k,v| set(k, v) }
-
return self
-
end
-
-
73
if respond_to?("#{option}=") and not ignore_setter
-
14
return __send__("#{option}=", value)
-
end
-
-
72
setter = proc { |val| set option, val, true }
-
10269
getter = proc { value }
-
-
59
case value
-
when Proc
-
18
getter = value
-
when Symbol, Integer, FalseClass, TrueClass, NilClass
-
25
getter = value.inspect
-
when Hash
-
5
setter = proc do |val|
-
val = value.merge val if Hash === val
-
set option, val, true
-
end
-
end
-
-
59
define_singleton("#{option}=", setter)
-
59
define_singleton(option, getter)
-
59
define_singleton("#{option}?", "!!#{option}") unless method_defined? "#{option}?"
-
59
self
-
end
-
-
# Same as calling `set :option, true` for each of the given options.
-
1
def enable(*opts)
-
opts.each { |key| set(key, true) }
-
end
-
-
# Same as calling `set :option, false` for each of the given options.
-
1
def disable(*opts)
-
opts.each { |key| set(key, false) }
-
end
-
-
# Define a custom error handler. Optionally takes either an Exception
-
# class, or an HTTP status code to specify which errors should be
-
# handled.
-
1
def error(*codes, &block)
-
3
args = compile! "ERROR", /.*/, block
-
3
codes = codes.flat_map(&method(:Array))
-
3
codes << Exception if codes.empty?
-
3
codes << Sinatra::NotFound if codes.include?(404)
-
6
codes.each { |c| (@errors[c] ||= []) << args }
-
end
-
-
# Sugar for `error(404) { ... }`
-
1
def not_found(&block)
-
error(404, &block)
-
end
-
-
# Define a named template. The block must return the template source.
-
1
def template(name, &block)
-
filename, line = caller_locations.first
-
templates[name] = [block, filename, line.to_i]
-
end
-
-
# Define the layout template. The block must return the template source.
-
1
def layout(name = :layout, &block)
-
template name, &block
-
end
-
-
# Load embedded templates from the file; uses the caller's __FILE__
-
# when no file is specified.
-
1
def inline_templates=(file = nil)
-
1
file = (file.nil? || file == true) ? (caller_files.first || File.expand_path($0)) : file
-
-
begin
-
1
io = ::IO.respond_to?(:binread) ? ::IO.binread(file) : ::IO.read(file)
-
1
app, data = io.gsub("\r\n", "\n").split(/^__END__$/, 2)
-
rescue Errno::ENOENT
-
app, data = nil
-
end
-
-
1
if data
-
if app and app =~ /([^\n]*\n)?#[^\n]*coding: *(\S+)/m
-
encoding = $2
-
else
-
encoding = settings.default_encoding
-
end
-
lines = app.count("\n") + 1
-
template = nil
-
force_encoding data, encoding
-
data.each_line do |line|
-
lines += 1
-
if line =~ /^@@\s*(.*\S)\s*$/
-
template = force_encoding(String.new, encoding)
-
templates[$1.to_sym] = [template, file, lines]
-
elsif template
-
template << line
-
end
-
end
-
end
-
end
-
-
# Lookup or register a mime type in Rack's mime registry.
-
1
def mime_type(type, value = nil)
-
20
return type if type.nil?
-
20
return type.to_s if type.to_s.include?('/')
-
16
type = ".#{type}" unless type.to_s[0] == ?.
-
16
return Rack::Mime.mime_type(type, nil) unless value
-
Rack::Mime::MIME_TYPES[type] = value
-
end
-
-
# provides all mime types matching type, including deprecated types:
-
# mime_types :html # => ['text/html']
-
# mime_types :js # => ['application/javascript', 'text/javascript']
-
1
def mime_types(type)
-
14
type = mime_type type
-
14
type =~ /^application\/(xml|javascript)$/ ? [type, "text/#$1"] : [type]
-
end
-
-
# Define a before filter; runs before all requests within the same
-
# context as route handlers and may access/modify the request and
-
# response.
-
1
def before(path = /.*/, **options, &block)
-
3
add_filter(:before, path, **options, &block)
-
end
-
-
# Define an after filter; runs after all requests within the same
-
# context as route handlers and may access/modify the request and
-
# response.
-
1
def after(path = /.*/, **options, &block)
-
add_filter(:after, path, **options, &block)
-
end
-
-
# add a filter
-
1
def add_filter(type, path = /.*/, **options, &block)
-
3
filters[type] << compile!(type, path, block, **options)
-
end
-
-
# Add a route condition. The route is considered non-matching when the
-
# block returns false.
-
1
def condition(name = "#{caller.first[/`.*'/]} condition", &block)
-
12
@conditions << generate_method(name, &block)
-
end
-
-
1
def public=(value)
-
warn ":public is no longer used to avoid overloading Module#public, use :public_folder or :public_dir instead"
-
set(:public_folder, value)
-
end
-
-
1
def public_dir=(value)
-
self.public_folder = value
-
end
-
-
1
def public_dir
-
public_folder
-
end
-
-
# Defining a `GET` handler also automatically defines
-
# a `HEAD` handler.
-
1
def get(path, opts = {}, &block)
-
7
conditions = @conditions.dup
-
7
route('GET', path, opts, &block)
-
-
7
@conditions = conditions
-
7
route('HEAD', path, opts, &block)
-
end
-
-
1
def put(path, opts = {}, &bk) route 'PUT', path, opts, &bk end
-
1
def post(path, opts = {}, &bk) route 'POST', path, opts, &bk end
-
1
def delete(path, opts = {}, &bk) route 'DELETE', path, opts, &bk end
-
1
def head(path, opts = {}, &bk) route 'HEAD', path, opts, &bk end
-
1
def options(path, opts = {}, &bk) route 'OPTIONS', path, opts, &bk end
-
1
def patch(path, opts = {}, &bk) route 'PATCH', path, opts, &bk end
-
1
def link(path, opts = {}, &bk) route 'LINK', path, opts, &bk end
-
1
def unlink(path, opts = {}, &bk) route 'UNLINK', path, opts, &bk end
-
-
# Makes the methods defined in the block and in the Modules given
-
# in `extensions` available to the handlers and templates
-
1
def helpers(*extensions, &block)
-
13
class_eval(&block) if block_given?
-
13
include(*extensions) if extensions.any?
-
end
-
-
# Register an extension. Alternatively take a block from which an
-
# extension will be created and registered on the fly.
-
1
def register(*extensions, &block)
-
11
extensions << Module.new(&block) if block_given?
-
11
@extensions += extensions
-
11
extensions.each do |extension|
-
15
extend extension
-
15
extension.registered(self) if extension.respond_to?(:registered)
-
end
-
end
-
-
7
def development?; environment == :development end
-
1
def production?; environment == :production end
-
1
def test?; environment == :test end
-
-
# Set configuration options for Sinatra and/or the app.
-
# Allows scoping of settings for certain environments.
-
1
def configure(*envs)
-
1
yield self if envs.empty? || envs.include?(environment.to_sym)
-
end
-
-
# Use the specified Rack middleware
-
1
def use(middleware, *args, &block)
-
@prototype = nil
-
@middleware << [middleware, args, block]
-
end
-
-
# Stop the self-hosted server if running.
-
1
def quit!
-
return unless running?
-
# Use Thin's hard #stop! if available, otherwise just #stop.
-
running_server.respond_to?(:stop!) ? running_server.stop! : running_server.stop
-
$stderr.puts "== Sinatra has ended his set (crowd applauds)" unless suppress_messages?
-
set :running_server, nil
-
set :handler_name, nil
-
end
-
-
1
alias_method :stop!, :quit!
-
-
# Run the Sinatra app as a self-hosted server using
-
# Thin, Puma, Mongrel, or WEBrick (in that order). If given a block, will call
-
# with the constructed handler once we have taken the stage.
-
1
def run!(options = {}, &block)
-
return if running?
-
set options
-
handler = detect_rack_handler
-
handler_name = handler.name.gsub(/.*::/, '')
-
server_settings = settings.respond_to?(:server_settings) ? settings.server_settings : {}
-
server_settings.merge!(:Port => port, :Host => bind)
-
-
begin
-
start_server(handler, server_settings, handler_name, &block)
-
rescue Errno::EADDRINUSE
-
$stderr.puts "== Someone is already performing on port #{port}!"
-
raise
-
ensure
-
quit!
-
end
-
end
-
-
1
alias_method :start!, :run!
-
-
# Check whether the self-hosted server is running or not.
-
1
def running?
-
running_server?
-
end
-
-
# The prototype instance used to process requests.
-
1
def prototype
-
@prototype ||= new
-
end
-
-
# Create a new instance without middleware in front of it.
-
1
alias new! new unless method_defined? :new!
-
-
# Create a new instance of the class fronted by its middleware
-
# pipeline. The object is guaranteed to respond to #call but may not be
-
# an instance of the class new was called on.
-
1
def new(*args, &bk)
-
2
instance = new!(*args, &bk)
-
2
Wrapper.new(build(instance).to_app, instance)
-
end
-
-
# Creates a Rack::Builder instance with all the middleware set up and
-
# the given +app+ as end point.
-
1
def build(app)
-
2
builder = Rack::Builder.new
-
2
setup_default_middleware builder
-
2
setup_middleware builder
-
2
builder.run app
-
2
builder
-
end
-
-
1
def call(env)
-
synchronize { prototype.call(env) }
-
end
-
-
# Like Kernel#caller but excluding certain magic entries and without
-
# line / method information; the resulting array contains filenames only.
-
1
def caller_files
-
5
cleaned_caller(1).flatten
-
end
-
-
# Like caller_files, but containing Arrays rather than strings with the
-
# first element being the file, and the second being the line.
-
1
def caller_locations
-
cleaned_caller 2
-
end
-
-
1
private
-
-
# Starts the server by running the Rack Handler.
-
1
def start_server(handler, server_settings, handler_name)
-
# Ensure we initialize middleware before startup, to match standard Rack
-
# behavior, by ensuring an instance exists:
-
prototype
-
# Run the instance we created:
-
handler.run(self, server_settings) do |server|
-
unless suppress_messages?
-
$stderr.puts "== Sinatra (v#{Sinatra::VERSION}) has taken the stage on #{port} for #{environment} with backup from #{handler_name}"
-
end
-
-
setup_traps
-
set :running_server, server
-
set :handler_name, handler_name
-
server.threaded = settings.threaded if server.respond_to? :threaded=
-
-
yield server if block_given?
-
end
-
end
-
-
1
def suppress_messages?
-
handler_name =~ /cgi/i || quiet
-
end
-
-
1
def setup_traps
-
if traps?
-
at_exit { quit! }
-
-
[:INT, :TERM].each do |signal|
-
old_handler = trap(signal) do
-
quit!
-
old_handler.call if old_handler.respond_to?(:call)
-
end
-
end
-
-
set :traps, false
-
end
-
end
-
-
# Dynamically defines a method on settings.
-
1
def define_singleton(name, content = Proc.new)
-
177
singleton_class.class_eval do
-
177
undef_method(name) if method_defined? name
-
177
String === content ? class_eval("def #{name}() #{content}; end") : define_method(name, &content)
-
end
-
end
-
-
# Condition for matching host name. Parameter might be String or Regexp.
-
1
def host_name(pattern)
-
condition { pattern === request.host }
-
end
-
-
# Condition for matching user agent. Parameter should be Regexp.
-
# Will set params[:agent].
-
1
def user_agent(pattern)
-
condition do
-
if request.user_agent.to_s =~ pattern
-
@params[:agent] = $~[1..-1]
-
true
-
else
-
false
-
end
-
end
-
end
-
1
alias_method :agent, :user_agent
-
-
# Condition for matching mimetypes. Accepts file extensions.
-
1
def provides(*types)
-
24
types.map! { |t| mime_types(t) }
-
12
types.flatten!
-
12
condition do
-
2
if type = response['Content-Type']
-
types.include? type or types.include? type[/^[^;]+/]
-
2
elsif type = request.preferred_type(types)
-
2
params = (type.respond_to?(:params) ? type.params : {})
-
2
content_type(type, params)
-
2
true
-
else
-
false
-
end
-
end
-
end
-
-
1
def route(verb, path, options = {}, &block)
-
14
enable :empty_path_info if path == "" and empty_path_info.nil?
-
14
signature = compile!(verb, path, block, **options)
-
14
(@routes[verb] ||= []) << signature
-
14
invoke_hook(:route_added, verb, path, block)
-
14
signature
-
end
-
-
1
def invoke_hook(name, *args)
-
110
extensions.each { |e| e.send(name, *args) if e.respond_to?(name) }
-
end
-
-
1
def generate_method(method_name, &block)
-
32
define_method(method_name, &block)
-
32
method = instance_method method_name
-
32
remove_method method_name
-
32
method
-
end
-
-
1
def compile!(verb, path, block, **options)
-
# Because of self.options.host
-
20
host_name(options.delete(:host)) if options.key?(:host)
-
# Pass Mustermann opts to compile()
-
20
route_mustermann_opts = options.key?(:mustermann_opts) ? options.delete(:mustermann_opts) : {}.freeze
-
-
32
options.each_pair { |option, args| send(option, *args) }
-
-
20
pattern = compile(path, route_mustermann_opts)
-
20
method_name = "#{verb} #{path}"
-
20
unbound_method = generate_method(method_name, &block)
-
20
conditions, @conditions = @conditions, []
-
20
wrapper = block.arity != 0 ?
-
proc { |a, p| unbound_method.bind(a).call(*p) } :
-
6
proc { |a, p| unbound_method.bind(a).call }
-
-
20
[ pattern, conditions, wrapper ]
-
end
-
-
1
def compile(path, route_mustermann_opts = {})
-
20
Mustermann.new(path, **mustermann_opts.merge(route_mustermann_opts))
-
end
-
-
1
def setup_default_middleware(builder)
-
2
builder.use ExtendedRack
-
2
builder.use ShowExceptions if show_exceptions?
-
2
builder.use Rack::MethodOverride if method_override?
-
2
builder.use Rack::Head
-
2
setup_logging builder
-
2
setup_sessions builder
-
2
setup_protection builder
-
end
-
-
1
def setup_middleware(builder)
-
2
middleware.each { |c,a,b| builder.use(c, *a, &b) }
-
end
-
-
1
def setup_logging(builder)
-
2
if logging?
-
setup_common_logger(builder)
-
setup_custom_logger(builder)
-
2
elsif logging == false
-
2
setup_null_logger(builder)
-
end
-
end
-
-
1
def setup_null_logger(builder)
-
2
builder.use Rack::NullLogger
-
end
-
-
1
def setup_common_logger(builder)
-
builder.use Sinatra::CommonLogger
-
end
-
-
1
def setup_custom_logger(builder)
-
if logging.respond_to? :to_int
-
builder.use Rack::Logger, logging
-
else
-
builder.use Rack::Logger
-
end
-
end
-
-
1
def setup_protection(builder)
-
2
return unless protection?
-
2
options = Hash === protection ? protection.dup : {}
-
2
options = {
-
img_src: "'self' data:",
-
font_src: "'self'"
-
}.merge options
-
-
4
protect_session = options.fetch(:session) { sessions? }
-
2
options[:without_session] = !protect_session
-
-
2
options[:reaction] ||= :drop_session
-
-
2
builder.use Rack::Protection, options
-
end
-
-
1
def setup_sessions(builder)
-
2
return unless sessions?
-
options = {}
-
options[:secret] = session_secret if session_secret?
-
options.merge! sessions.to_hash if sessions.respond_to? :to_hash
-
builder.use session_store, options
-
end
-
-
1
def detect_rack_handler
-
servers = Array(server)
-
servers.each do |server_name|
-
begin
-
return Rack::Handler.get(server_name.to_s)
-
rescue LoadError, NameError
-
end
-
end
-
fail "Server handler (#{servers.join(',')}) not found."
-
end
-
-
1
def inherited(subclass)
-
3
subclass.reset!
-
3
subclass.set :app_file, caller_files.first unless subclass.app_file?
-
3
super
-
end
-
-
1
@@mutex = Mutex.new
-
1
def synchronize(&block)
-
if lock?
-
@@mutex.synchronize(&block)
-
else
-
yield
-
end
-
end
-
-
# used for deprecation warnings
-
1
def warn(message)
-
super message + "\n\tfrom #{cleaned_caller.first.join(':')}"
-
end
-
-
# Like Kernel#caller but excluding certain magic entries
-
1
def cleaned_caller(keep = 3)
-
5
caller(1).
-
108
map! { |line| line.split(/:(?=\d|in )/, 3)[0,keep] }.
-
765
reject { |file, *_| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } }
-
end
-
end
-
-
# Force data to specified encoding. It defaults to settings.default_encoding
-
# which is UTF-8 by default
-
1
def self.force_encoding(data, encoding = default_encoding)
-
6
return if data == settings || data.is_a?(Tempfile)
-
6
if data.respond_to? :force_encoding
-
data.force_encoding(encoding).encode!
-
6
elsif data.respond_to? :each_value
-
6
data.each_value { |v| force_encoding(v, encoding) }
-
elsif data.respond_to? :each
-
data.each { |v| force_encoding(v, encoding) }
-
end
-
6
data
-
end
-
-
7
def force_encoding(*args) settings.force_encoding(*args) end
-
-
1
reset!
-
-
1
set :environment, (ENV['APP_ENV'] || ENV['RACK_ENV'] || :development).to_sym
-
1
set :raise_errors, Proc.new { test? }
-
1
set :dump_errors, Proc.new { !test? }
-
1
set :show_exceptions, Proc.new { development? }
-
1
set :sessions, false
-
1
set :session_store, Rack::Session::Cookie
-
1
set :logging, false
-
1
set :protection, true
-
1
set :method_override, false
-
1
set :use_code, false
-
1
set :default_encoding, "utf-8"
-
1
set :x_cascade, true
-
4
set :add_charset, %w[javascript xml xhtml+xml].map { |t| "application/#{t}" }
-
1
settings.add_charset << /^text\//
-
1
set :mustermann_opts, {}
-
-
# explicitly generating a session secret eagerly to play nice with preforking
-
begin
-
1
require 'securerandom'
-
1
set :session_secret, SecureRandom.hex(64)
-
rescue LoadError, NotImplementedError
-
# SecureRandom raises a NotImplementedError if no random device is available
-
set :session_secret, "%064x" % Kernel.rand(2**256-1)
-
end
-
-
1
class << self
-
1
alias_method :methodoverride?, :method_override?
-
1
alias_method :methodoverride=, :method_override=
-
end
-
-
1
set :run, false # start server via at-exit hook?
-
1
set :running_server, nil
-
1
set :handler_name, nil
-
1
set :traps, true
-
1
set :server, %w[HTTP webrick]
-
1
set :bind, Proc.new { development? ? 'localhost' : '0.0.0.0' }
-
1
set :port, Integer(ENV['PORT'] && !ENV['PORT'].empty? ? ENV['PORT'] : 4567)
-
1
set :quiet, false
-
-
1
ruby_engine = defined?(RUBY_ENGINE) && RUBY_ENGINE
-
-
1
if ruby_engine == 'macruby'
-
server.unshift 'control_tower'
-
else
-
1
server.unshift 'reel'
-
1
server.unshift 'puma'
-
1
server.unshift 'mongrel' if ruby_engine.nil?
-
1
server.unshift 'thin' if ruby_engine != 'jruby'
-
1
server.unshift 'trinidad' if ruby_engine == 'jruby'
-
end
-
-
1
set :absolute_redirects, true
-
1
set :prefixed_redirects, false
-
1
set :empty_path_info, nil
-
1
set :strict_paths, true
-
-
1
set :app_file, nil
-
9
set :root, Proc.new { app_file && File.expand_path(File.dirname(app_file)) }
-
1
set :views, Proc.new { root && File.join(root, 'views') }
-
1
set :reload_templates, Proc.new { development? }
-
1
set :lock, false
-
1
set :threaded, true
-
-
5
set :public_folder, Proc.new { root && File.join(root, 'public') }
-
3
set :static, Proc.new { public_folder && File.exist?(public_folder) }
-
1
set :static_cache_control, false
-
-
1
error ::Exception do
-
response.status = 500
-
content_type 'text/html'
-
'<h1>Internal Server Error</h1>'
-
end
-
-
1
configure :development do
-
1
get '/__sinatra__/:image.png' do
-
filename = File.dirname(__FILE__) + "/images/#{params[:image].to_i}.png"
-
content_type :png
-
send_file filename
-
end
-
-
1
error NotFound do
-
content_type 'text/html'
-
-
if self.class == Sinatra::Application
-
code = <<-RUBY.gsub(/^ {12}/, '')
-
#{request.request_method.downcase} '#{request.path_info}' do
-
"Hello World"
-
end
-
RUBY
-
else
-
code = <<-RUBY.gsub(/^ {12}/, '')
-
class #{self.class}
-
#{request.request_method.downcase} '#{request.path_info}' do
-
"Hello World"
-
end
-
end
-
RUBY
-
-
file = settings.app_file.to_s.sub(settings.root.to_s, '').sub(/^\//, '')
-
code = "# in #{file}\n#{code}" unless file.empty?
-
end
-
-
(<<-HTML).gsub(/^ {10}/, '')
-
<!DOCTYPE html>
-
<html>
-
<head>
-
<style type="text/css">
-
body { text-align:center;font-family:helvetica,arial;font-size:22px;
-
color:#888;margin:20px}
-
#c {margin:0 auto;width:500px;text-align:left}
-
</style>
-
</head>
-
<body>
-
<h2>Sinatra doesn’t know this ditty.</h2>
-
<img src='#{uri "/__sinatra__/404.png"}'>
-
<div id="c">
-
Try this:
-
<pre>#{Rack::Utils.escape_html(code)}</pre>
-
</div>
-
</body>
-
</html>
-
HTML
-
end
-
end
-
end
-
-
# Execution context for classic style (top-level) applications. All
-
# DSL methods executed on main are delegated to this class.
-
#
-
# The Application class should not be subclassed, unless you want to
-
# inherit all settings, routes, handlers, and error pages from the
-
# top-level. Subclassing Sinatra::Base is highly recommended for
-
# modular applications.
-
1
class Application < Base
-
1
set :logging, Proc.new { !test? }
-
1
set :method_override, true
-
1
set :run, Proc.new { !test? }
-
1
set :app_file, nil
-
-
1
def self.register(*extensions, &block) #:nodoc:
-
6
added_methods = extensions.flat_map(&:public_instance_methods)
-
6
Delegator.delegate(*added_methods)
-
6
super(*extensions, &block)
-
end
-
end
-
-
# Sinatra delegation mixin. Mixing this module into an object causes all
-
# methods to be delegated to the Sinatra::Application class. Used primarily
-
# at the top-level.
-
1
module Delegator #:nodoc:
-
1
def self.delegate(*methods)
-
11
methods.each do |method_name|
-
46
define_method(method_name) do |*args, &block|
-
return super(*args, &block) if respond_to? method_name
-
Delegator.target.send(method_name, *args, &block)
-
end
-
46
private method_name
-
end
-
end
-
-
1
delegate :get, :patch, :put, :post, :delete, :head, :options, :link, :unlink,
-
:template, :layout, :before, :after, :error, :not_found, :configure,
-
:set, :mime_type, :enable, :disable, :use, :development?, :test?,
-
:production?, :helpers, :settings, :register
-
-
1
class << self
-
1
attr_accessor :target
-
end
-
-
1
self.target = Application
-
end
-
-
1
class Wrapper
-
1
def initialize(stack, instance)
-
2
@stack, @instance = stack, instance
-
end
-
-
1
def settings
-
@instance.settings
-
end
-
-
1
def helpers
-
@instance
-
end
-
-
1
def call(env)
-
2
@stack.call(env)
-
end
-
-
1
def inspect
-
"#<#{@instance.class} app_file=#{settings.app_file.inspect}>"
-
end
-
end
-
-
# Create a new Sinatra application; the block is evaluated in the class scope.
-
1
def self.new(base = Base, &block)
-
base = Class.new(base)
-
base.class_eval(&block) if block_given?
-
base
-
end
-
-
# Extend the top-level DSL with the modules provided.
-
1
def self.register(*extensions, &block)
-
6
Delegator.target.register(*extensions, &block)
-
end
-
-
# Include the helper modules provided in Sinatra's request context.
-
1
def self.helpers(*extensions, &block)
-
7
Delegator.target.helpers(*extensions, &block)
-
end
-
-
# Use the middleware for classic applications.
-
1
def self.use(*args, &block)
-
Delegator.target.use(*args, &block)
-
end
-
end
-
# frozen_string_literal: true
-
1
$stderr.puts <<EOF if !Hash.method_defined?(:slice) && !$LOAD_PATH.grep(%r{gems/activesupport}).empty? && ENV['SINATRA_ACTIVESUPPORT_WARNING'] != 'false'
-
WARNING: If you plan to load any of ActiveSupport's core extensions to Hash, be
-
sure to do so *before* loading Sinatra::Application or Sinatra::Base. If not,
-
you may disregard this warning.
-
-
Set SINATRA_ACTIVESUPPORT_WARNING=false in the environment to hide this warning.
-
EOF
-
-
1
module Sinatra
-
# A poor man's ActiveSupport::HashWithIndifferentAccess, with all the Rails-y
-
# stuff removed.
-
#
-
# Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are
-
# considered to be the same.
-
#
-
# rgb = Sinatra::IndifferentHash.new
-
#
-
# rgb[:black] = '#000000' # symbol assignment
-
# rgb[:black] # => '#000000' # symbol retrieval
-
# rgb['black'] # => '#000000' # string retrieval
-
#
-
# rgb['white'] = '#FFFFFF' # string assignment
-
# rgb[:white] # => '#FFFFFF' # symbol retrieval
-
# rgb['white'] # => '#FFFFFF' # string retrieval
-
#
-
# Internally, symbols are mapped to strings when used as keys in the entire
-
# writing interface (calling e.g. <tt>[]=</tt>, <tt>merge</tt>). This mapping
-
# belongs to the public interface. For example, given:
-
#
-
# hash = Sinatra::IndifferentHash.new(:a=>1)
-
#
-
# You are guaranteed that the key is returned as a string:
-
#
-
# hash.keys # => ["a"]
-
#
-
# Technically other types of keys are accepted:
-
#
-
# hash = Sinatra::IndifferentHash.new(:a=>1)
-
# hash[0] = 0
-
# hash # => { "a"=>1, 0=>0 }
-
#
-
# But this class is intended for use cases where strings or symbols are the
-
# expected keys and it is convenient to understand both as the same. For
-
# example the +params+ hash in Sinatra.
-
1
class IndifferentHash < Hash
-
1
def self.[](*args)
-
new.merge!(Hash[*args])
-
end
-
-
1
def initialize(*args)
-
2
args.map!(&method(:convert_value))
-
-
2
super(*args)
-
end
-
-
1
def default(*args)
-
args.map!(&method(:convert_key))
-
-
super(*args)
-
end
-
-
1
def default=(value)
-
super(convert_value(value))
-
end
-
-
1
def assoc(key)
-
super(convert_key(key))
-
end
-
-
1
def rassoc(value)
-
super(convert_value(value))
-
end
-
-
1
def fetch(key, *args)
-
args.map!(&method(:convert_value))
-
-
super(convert_key(key), *args)
-
end
-
-
1
def [](key)
-
super(convert_key(key))
-
end
-
-
1
def []=(key, value)
-
super(convert_key(key), convert_value(value))
-
end
-
-
1
alias_method :store, :[]=
-
-
1
def key(value)
-
super(convert_value(value))
-
end
-
-
1
def key?(key)
-
super(convert_key(key))
-
end
-
-
1
alias_method :has_key?, :key?
-
1
alias_method :include?, :key?
-
1
alias_method :member?, :key?
-
-
1
def value?(value)
-
super(convert_value(value))
-
end
-
-
1
alias_method :has_value?, :value?
-
-
1
def delete(key)
-
super(convert_key(key))
-
end
-
-
def dig(key, *other_keys)
-
super(convert_key(key), *other_keys)
-
1
end if method_defined?(:dig) # Added in Ruby 2.3
-
-
def fetch_values(*keys)
-
keys.map!(&method(:convert_key))
-
-
super(*keys)
-
1
end if method_defined?(:fetch_values) # Added in Ruby 2.3
-
-
def slice(*keys)
-
keys.map!(&method(:convert_key))
-
-
self.class[super(*keys)]
-
1
end if method_defined?(:slice) # Added in Ruby 2.5
-
-
1
def values_at(*keys)
-
keys.map!(&method(:convert_key))
-
-
super(*keys)
-
end
-
-
1
def merge!(*other_hashes)
-
2
other_hashes.each do |other_hash|
-
2
if other_hash.is_a?(self.class)
-
super(other_hash)
-
else
-
2
other_hash.each_pair do |key, value|
-
key = convert_key(key)
-
value = yield(key, self[key], value) if block_given? && key?(key)
-
self[key] = convert_value(value)
-
end
-
end
-
end
-
-
2
self
-
end
-
-
1
alias_method :update, :merge!
-
-
1
def merge(*other_hashes, &block)
-
dup.merge!(*other_hashes, &block)
-
end
-
-
1
def replace(other_hash)
-
super(other_hash.is_a?(self.class) ? other_hash : self.class[other_hash])
-
end
-
-
1
if method_defined?(:transform_values!) # Added in Ruby 2.4
-
1
def transform_values(&block)
-
dup.transform_values!(&block)
-
end
-
-
1
def transform_values!
-
super
-
super(&method(:convert_value))
-
end
-
end
-
-
1
if method_defined?(:transform_keys!) # Added in Ruby 2.5
-
1
def transform_keys(&block)
-
dup.transform_keys!(&block)
-
end
-
-
1
def transform_keys!
-
super
-
super(&method(:convert_key))
-
end
-
end
-
-
1
private
-
-
1
def convert_key(key)
-
key.is_a?(Symbol) ? key.to_s : key
-
end
-
-
1
def convert_value(value)
-
case value
-
when Hash
-
value.is_a?(self.class) ? value : self.class[value]
-
when Array
-
value.map(&method(:convert_value))
-
else
-
value
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require 'rack/show_exceptions'
-
-
1
module Sinatra
-
# Sinatra::ShowExceptions catches all exceptions raised from the app it
-
# wraps. It shows a useful backtrace with the sourcefile and clickable
-
# context, the whole Rack environment and the request data.
-
#
-
# Be careful when you use this on public-facing sites as it could reveal
-
# information helpful to attackers.
-
1
class ShowExceptions < Rack::ShowExceptions
-
1
@@eats_errors = Object.new
-
1
def @@eats_errors.flush(*) end
-
1
def @@eats_errors.puts(*) end
-
-
1
def initialize(app)
-
@app = app
-
end
-
-
1
def call(env)
-
@app.call(env)
-
rescue Exception => e
-
errors, env["rack.errors"] = env["rack.errors"], @@eats_errors
-
-
if prefers_plain_text?(env)
-
content_type = "text/plain"
-
body = dump_exception(e)
-
else
-
content_type = "text/html"
-
body = pretty(env, e)
-
end
-
-
env["rack.errors"] = errors
-
-
[
-
500,
-
{
-
"Content-Type" => content_type,
-
"Content-Length" => body.bytesize.to_s
-
},
-
[body]
-
]
-
end
-
-
# Pulled from Rack::ShowExceptions in order to override TEMPLATE.
-
# If Rack provides another way to override, this could be removed
-
# in the future.
-
1
def pretty(env, exception)
-
req = Rack::Request.new(env)
-
-
# This double assignment is to prevent an "unused variable" warning on
-
# Ruby 1.9.3. Yes, it is dumb, but I don't like Ruby yelling at me.
-
path = path = (req.script_name + req.path_info).squeeze("/")
-
-
# This double assignment is to prevent an "unused variable" warning on
-
# Ruby 1.9.3. Yes, it is dumb, but I don't like Ruby yelling at me.
-
frames = frames = exception.backtrace.map { |line|
-
frame = OpenStruct.new
-
if line =~ /(.*?):(\d+)(:in `(.*)')?/
-
frame.filename = $1
-
frame.lineno = $2.to_i
-
frame.function = $4
-
-
begin
-
lineno = frame.lineno-1
-
lines = ::File.readlines(frame.filename)
-
frame.pre_context_lineno = [lineno-CONTEXT, 0].max
-
frame.pre_context = lines[frame.pre_context_lineno...lineno]
-
frame.context_line = lines[lineno].chomp
-
frame.post_context_lineno = [lineno+CONTEXT, lines.size].min
-
frame.post_context = lines[lineno+1..frame.post_context_lineno]
-
rescue
-
end
-
-
frame
-
else
-
nil
-
end
-
}.compact
-
-
TEMPLATE.result(binding)
-
end
-
-
1
private
-
-
1
def bad_request?(e)
-
Sinatra::BadRequest === e
-
end
-
-
1
def prefers_plain_text?(env)
-
!(Request.new(env).preferred_type("text/plain","text/html") == "text/html") &&
-
[/curl/].index { |item| item =~ env["HTTP_USER_AGENT"] }
-
end
-
-
1
def frame_class(frame)
-
if frame.filename =~ %r{lib/sinatra.*\.rb}
-
"framework"
-
elsif (defined?(Gem) && frame.filename.include?(Gem.dir)) ||
-
frame.filename =~ %r{/bin/(\w+)\z}
-
"system"
-
else
-
"app"
-
end
-
end
-
-
1
TEMPLATE = ERB.new <<-HTML # :nodoc:
-
<!DOCTYPE html>
-
<html>
-
<head>
-
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
-
<title><%=h exception.class %> at <%=h path %></title>
-
-
<script type="text/javascript">
-
//<!--
-
function toggle(id) {
-
var pre = document.getElementById("pre-" + id);
-
var post = document.getElementById("post-" + id);
-
var context = document.getElementById("context-" + id);
-
-
if (pre.style.display == 'block') {
-
pre.style.display = 'none';
-
post.style.display = 'none';
-
context.style.background = "none";
-
} else {
-
pre.style.display = 'block';
-
post.style.display = 'block';
-
context.style.background = "#fffed9";
-
}
-
}
-
-
function toggleBacktrace(){
-
var bt = document.getElementById("backtrace");
-
var toggler = document.getElementById("expando");
-
-
if (bt.className == 'condensed') {
-
bt.className = 'expanded';
-
toggler.innerHTML = "(condense)";
-
} else {
-
bt.className = 'condensed';
-
toggler.innerHTML = "(expand)";
-
}
-
}
-
//-->
-
</script>
-
-
<style type="text/css" media="screen">
-
* {margin: 0; padding: 0; border: 0; outline: 0;}
-
div.clear {clear: both;}
-
body {background: #EEEEEE; margin: 0; padding: 0;
-
font-family: 'Lucida Grande', 'Lucida Sans Unicode',
-
'Garuda';}
-
code {font-family: 'Lucida Console', monospace;
-
font-size: 12px;}
-
li {height: 18px;}
-
ul {list-style: none; margin: 0; padding: 0;}
-
ol:hover {cursor: pointer;}
-
ol li {white-space: pre;}
-
#explanation {font-size: 12px; color: #666666;
-
margin: 20px 0 0 100px;}
-
/* WRAP */
-
#wrap {width: 1000px; background: #FFFFFF; margin: 0 auto;
-
padding: 30px 50px 20px 50px;
-
border-left: 1px solid #DDDDDD;
-
border-right: 1px solid #DDDDDD;}
-
/* HEADER */
-
#header {margin: 0 auto 25px auto;}
-
#header img {float: left;}
-
#header #summary {float: left; margin: 12px 0 0 20px; width:660px;
-
font-family: 'Lucida Grande', 'Lucida Sans Unicode';}
-
h1 {margin: 0; font-size: 36px; color: #981919;}
-
h2 {margin: 0; font-size: 22px; color: #333333;}
-
#header ul {margin: 0; font-size: 12px; color: #666666;}
-
#header ul li strong{color: #444444;}
-
#header ul li {display: inline; padding: 0 10px;}
-
#header ul li.first {padding-left: 0;}
-
#header ul li.last {border: 0; padding-right: 0;}
-
/* BODY */
-
#backtrace,
-
#get,
-
#post,
-
#cookies,
-
#rack {width: 980px; margin: 0 auto 10px auto;}
-
p#nav {float: right; font-size: 14px;}
-
/* BACKTRACE */
-
a#expando {float: left; padding-left: 5px; color: #666666;
-
font-size: 14px; text-decoration: none; cursor: pointer;}
-
a#expando:hover {text-decoration: underline;}
-
h3 {float: left; width: 100px; margin-bottom: 10px;
-
color: #981919; font-size: 14px; font-weight: bold;}
-
#nav a {color: #666666; text-decoration: none; padding: 0 5px;}
-
#backtrace li.frame-info {background: #f7f7f7; padding-left: 10px;
-
font-size: 12px; color: #333333;}
-
#backtrace ul {list-style-position: outside; border: 1px solid #E9E9E9;
-
border-bottom: 0;}
-
#backtrace ol {width: 920px; margin-left: 50px;
-
font: 10px 'Lucida Console', monospace; color: #666666;}
-
#backtrace ol li {border: 0; border-left: 1px solid #E9E9E9;
-
padding: 2px 0;}
-
#backtrace ol code {font-size: 10px; color: #555555; padding-left: 5px;}
-
#backtrace-ul li {border-bottom: 1px solid #E9E9E9; height: auto;
-
padding: 3px 0;}
-
#backtrace-ul .code {padding: 6px 0 4px 0;}
-
#backtrace.condensed .system,
-
#backtrace.condensed .framework {display:none;}
-
/* REQUEST DATA */
-
p.no-data {padding-top: 2px; font-size: 12px; color: #666666;}
-
table.req {width: 980px; text-align: left; font-size: 12px;
-
color: #666666; padding: 0; border-spacing: 0;
-
border: 1px solid #EEEEEE; border-bottom: 0;
-
border-left: 0;
-
clear:both}
-
table.req tr th {padding: 2px 10px; font-weight: bold;
-
background: #F7F7F7; border-bottom: 1px solid #EEEEEE;
-
border-left: 1px solid #EEEEEE;}
-
table.req tr td {padding: 2px 20px 2px 10px;
-
border-bottom: 1px solid #EEEEEE;
-
border-left: 1px solid #EEEEEE;}
-
/* HIDE PRE/POST CODE AT START */
-
.pre-context,
-
.post-context {display: none;}
-
-
table td.code {width:750px}
-
table td.code div {width:750px;overflow:hidden}
-
</style>
-
</head>
-
<body>
-
<div id="wrap">
-
<div id="header">
-
<img src="<%= env['SCRIPT_NAME'] %>/__sinatra__/500.png" alt="application error" height="161" width="313" />
-
<div id="summary">
-
<h1><strong><%=h exception.class %></strong> at <strong><%=h path %>
-
</strong></h1>
-
<h2><%=h exception.message %></h2>
-
<ul>
-
<li class="first"><strong>file:</strong> <code>
-
<%=h frames.first.filename.split("/").last %></code></li>
-
<li><strong>location:</strong> <code><%=h frames.first.function %>
-
</code></li>
-
<li class="last"><strong>line:
-
</strong> <%=h frames.first.lineno %></li>
-
</ul>
-
</div>
-
<div class="clear"></div>
-
</div>
-
-
<div id="backtrace" class='condensed'>
-
<h3>BACKTRACE</h3>
-
<p><a href="#" id="expando"
-
onclick="toggleBacktrace(); return false">(expand)</a></p>
-
<p id="nav"><strong>JUMP TO:</strong>
-
<% unless bad_request?(exception) %>
-
<a href="#get-info">GET</a>
-
<a href="#post-info">POST</a>
-
<% end %>
-
<a href="#cookie-info">COOKIES</a>
-
<a href="#env-info">ENV</a>
-
</p>
-
<div class="clear"></div>
-
-
<ul id="backtrace-ul">
-
-
<% id = 1 %>
-
<% frames.each do |frame| %>
-
<% if frame.context_line && frame.context_line != "#" %>
-
-
<li class="frame-info <%= frame_class(frame) %>">
-
<code><%=h frame.filename %></code> in
-
<code><strong><%=h frame.function %></strong></code>
-
</li>
-
-
<li class="code <%= frame_class(frame) %>">
-
<% if frame.pre_context %>
-
<ol start="<%=h frame.pre_context_lineno + 1 %>"
-
class="pre-context" id="pre-<%= id %>"
-
onclick="toggle(<%= id %>);">
-
<% frame.pre_context.each do |line| %>
-
<li class="pre-context-line"><code><%=h line %></code></li>
-
<% end %>
-
</ol>
-
<% end %>
-
-
<ol start="<%= frame.lineno %>" class="context" id="<%= id %>"
-
onclick="toggle(<%= id %>);">
-
<li class="context-line" id="context-<%= id %>"><code><%=
-
h frame.context_line %></code></li>
-
</ol>
-
-
<% if frame.post_context %>
-
<ol start="<%=h frame.lineno + 1 %>" class="post-context"
-
id="post-<%= id %>" onclick="toggle(<%= id %>);">
-
<% frame.post_context.each do |line| %>
-
<li class="post-context-line"><code><%=h line %></code></li>
-
<% end %>
-
</ol>
-
<% end %>
-
<div class="clear"></div>
-
</li>
-
-
<% end %>
-
-
<% id += 1 %>
-
<% end %>
-
-
</ul>
-
</div> <!-- /BACKTRACE -->
-
-
<% unless bad_request?(exception) %>
-
<div id="get">
-
<h3 id="get-info">GET</h3>
-
<% if req.GET and not req.GET.empty? %>
-
<table class="req">
-
<tr>
-
<th>Variable</th>
-
<th>Value</th>
-
</tr>
-
<% req.GET.sort_by { |k, v| k.to_s }.each { |key, val| %>
-
<tr>
-
<td><%=h key %></td>
-
<td class="code"><div><%=h val.inspect %></div></td>
-
</tr>
-
<% } %>
-
</table>
-
<% else %>
-
<p class="no-data">No GET data.</p>
-
<% end %>
-
<div class="clear"></div>
-
</div> <!-- /GET -->
-
-
<div id="post">
-
<h3 id="post-info">POST</h3>
-
<% if req.POST and not req.POST.empty? %>
-
<table class="req">
-
<tr>
-
<th>Variable</th>
-
<th>Value</th>
-
</tr>
-
<% req.POST.sort_by { |k, v| k.to_s }.each { |key, val| %>
-
<tr>
-
<td><%=h key %></td>
-
<td class="code"><div><%=h val.inspect %></div></td>
-
</tr>
-
<% } %>
-
</table>
-
<% else %>
-
<p class="no-data">No POST data.</p>
-
<% end %>
-
<div class="clear"></div>
-
</div> <!-- /POST -->
-
<% end %>
-
-
<div id="cookies">
-
<h3 id="cookie-info">COOKIES</h3>
-
<% unless req.cookies.empty? %>
-
<table class="req">
-
<tr>
-
<th>Variable</th>
-
<th>Value</th>
-
</tr>
-
<% req.cookies.each { |key, val| %>
-
<tr>
-
<td><%=h key %></td>
-
<td class="code"><div><%=h val.inspect %></div></td>
-
</tr>
-
<% } %>
-
</table>
-
<% else %>
-
<p class="no-data">No cookie data.</p>
-
<% end %>
-
<div class="clear"></div>
-
</div> <!-- /COOKIES -->
-
-
<div id="rack">
-
<h3 id="env-info">Rack ENV</h3>
-
<table class="req">
-
<tr>
-
<th>Variable</th>
-
<th>Value</th>
-
</tr>
-
<% env.sort_by { |k, v| k.to_s }.each { |key, val| %>
-
<tr>
-
<td><%=h key %></td>
-
<td class="code"><div><%=h val %></div></td>
-
</tr>
-
<% } %>
-
</table>
-
<div class="clear"></div>
-
</div> <!-- /RACK ENV -->
-
-
<p id="explanation">You're seeing this error because you have
-
enabled the <code>show_exceptions</code> setting.</p>
-
</div> <!-- /WRAP -->
-
</body>
-
</html>
-
HTML
-
end
-
end
-
1
module Sinatra
-
1
VERSION = '2.0.8.1'
-
end
-
1
require 'sinatra/base'
-
1
require 'yaml'
-
1
require 'erb'
-
-
1
module Sinatra
-
-
# = Sinatra::ConfigFile
-
#
-
# <tt>Sinatra::ConfigFile</tt> is an extension that allows you to load the
-
# application's configuration from YAML files. It automatically detects if
-
# the files contain specific environment settings and it will use those
-
# corresponding to the current one.
-
#
-
# You can access those options through +settings+ within the application. If
-
# you try to get the value for a setting that hasn't been defined in the
-
# config file for the current environment, you will get whatever it was set
-
# to in the application.
-
#
-
# == Usage
-
#
-
# Once you have written your configurations to a YAML file you can tell the
-
# extension to load them. See below for more information about how these
-
# files are interpreted.
-
#
-
# For the examples, lets assume the following config.yml file:
-
#
-
# greeting: Welcome to my file configurable application
-
#
-
# === Classic Application
-
#
-
# require "sinatra"
-
# require "sinatra/config_file"
-
#
-
# config_file 'path/to/config.yml'
-
#
-
# get '/' do
-
# @greeting = settings.greeting
-
# haml :index
-
# end
-
#
-
# # The rest of your classic application code goes here...
-
#
-
# === Modular Application
-
#
-
# require "sinatra/base"
-
# require "sinatra/config_file"
-
#
-
# class MyApp < Sinatra::Base
-
# register Sinatra::ConfigFile
-
#
-
# config_file 'path/to/config.yml'
-
#
-
# get '/' do
-
# @greeting = settings.greeting
-
# haml :index
-
# end
-
#
-
# # The rest of your modular application code goes here...
-
# end
-
#
-
# === Config File Format
-
#
-
# In its most simple form this file is just a key-value list:
-
#
-
# foo: bar
-
# something: 42
-
# nested:
-
# a: 1
-
# b: 2
-
#
-
# But it also can provide specific environment configuration. There are two
-
# ways to do that: at the file level and at the settings level.
-
#
-
# At the settings level (e.g. in 'path/to/config.yml'):
-
#
-
# development:
-
# foo: development
-
# bar: bar
-
# test:
-
# foo: test
-
# bar: bar
-
# production:
-
# foo: production
-
# bar: bar
-
#
-
# Or at the file level:
-
#
-
# foo:
-
# development: development
-
# test: test
-
# production: production
-
# bar: bar
-
#
-
# In either case, <tt>settings.foo</tt> will return the environment name, and
-
# <tt>settings.bar</tt> will return <tt>"bar"</tt>.
-
#
-
# If you wish to provide defaults that may be shared among all the
-
# environments, this can be done by using a YAML alias, and then overwriting
-
# values in environments where appropriate:
-
#
-
# default: &common_settings
-
# foo: 'foo'
-
# bar: 'bar'
-
#
-
# production:
-
# <<: *common_settings
-
# bar: 'baz' # override the default value
-
#
-
1
module ConfigFile
-
-
# When the extension is registered sets the +environments+ setting to the
-
# traditional environments: development, test and production.
-
1
def self.registered(base)
-
2
base.set :environments, %w[test production development]
-
end
-
-
# Loads the configuration from the YAML files whose +paths+ are passed as
-
# arguments, filtering the settings for the current environment. Note that
-
# these +paths+ can actually be globs.
-
1
def config_file(*paths)
-
Dir.chdir(root || '.') do
-
paths.each do |pattern|
-
Dir.glob(pattern) do |file|
-
raise UnsupportedConfigType unless ['.yml', '.yaml', '.erb'].include?(File.extname(file))
-
logger.info "loading config file '#{file}'" if logging? && respond_to?(:logger)
-
document = ERB.new(IO.read(file)).result
-
yaml = YAML.load(document)
-
config = config_for_env(yaml)
-
config.each_pair { |key, value| set(key, value) }
-
end
-
end
-
end
-
end
-
-
1
class UnsupportedConfigType < Exception
-
1
def message
-
'Invalid config file type, use .yml, .yaml or .erb'
-
end
-
end
-
-
1
private
-
-
# Given a +hash+ containing application configuration it returns
-
# settings applicable to the current environment. Note: It gives
-
# precedence to environment settings defined at the root-level.
-
1
def config_for_env(hash)
-
return from_environment_key(hash) if environment_keys?(hash)
-
-
hash.each_with_object(IndifferentHash[]) do |(k, v), acc|
-
if environment_keys?(v)
-
acc.merge!(k => v[environment.to_s]) if v.key?(environment.to_s)
-
else
-
acc.merge!(k => v)
-
end
-
end
-
end
-
-
# Given a +hash+ returns the settings corresponding to the current
-
# environment.
-
1
def from_environment_key(hash)
-
hash[environment.to_s] || hash[environment.to_sym] || {}
-
end
-
-
# Returns true if supplied with a hash that has any recognized
-
# +environments+ in its root keys.
-
1
def environment_keys?(hash)
-
hash.is_a?(Hash) && hash.any? { |k, _| environments.include?(k.to_s) }
-
end
-
end
-
-
1
register ConfigFile
-
1
Delegator.delegate :config_file
-
end
-
1
require 'sinatra/base'
-
1
require 'sinatra/capture'
-
-
1
module Sinatra
-
-
# = Sinatra::ContentFor
-
#
-
# <tt>Sinatra::ContentFor</tt> is a set of helpers that allows you to capture
-
# blocks inside views to be rendered later during the request. The most
-
# common use is to populate different parts of your layout from your view.
-
#
-
# The currently supported engines are: Erb, Erubi, Erubis, Haml and Slim.
-
#
-
# == Usage
-
#
-
# You call +content_for+, generally from a view, to capture a block of markup
-
# giving it an identifier:
-
#
-
# # index.erb
-
# <% content_for :some_key do %>
-
# <chunk of="html">...</chunk>
-
# <% end %>
-
#
-
# Then, you call +yield_content+ with that identifier, generally from a
-
# layout, to render the captured block:
-
#
-
# # layout.erb
-
# <%= yield_content :some_key %>
-
#
-
# If you have provided +yield_content+ with a block and no content for the
-
# specified key is found, it will render the results of the block provided
-
# to yield_content.
-
#
-
# # layout.erb
-
# <% yield_content :some_key_with_no_content do %>
-
# <chunk of="default html">...</chunk>
-
# <% end %>
-
#
-
# === Classic Application
-
#
-
# To use the helpers in a classic application all you need to do is require
-
# them:
-
#
-
# require "sinatra"
-
# require "sinatra/content_for"
-
#
-
# # Your classic application code goes here...
-
#
-
# === Modular Application
-
#
-
# To use the helpers in a modular application you need to require them, and
-
# then, tell the application you will use them:
-
#
-
# require "sinatra/base"
-
# require "sinatra/content_for"
-
#
-
# class MyApp < Sinatra::Base
-
# helpers Sinatra::ContentFor
-
#
-
# # The rest of your modular application code goes here...
-
# end
-
#
-
# == And How Is This Useful?
-
#
-
# For example, some of your views might need a few javascript tags and
-
# stylesheets, but you don't want to force this files in all your pages.
-
# Then you can put <tt><%= yield_content :scripts_and_styles %></tt> on your
-
# layout, inside the <head> tag, and each view can call <tt>content_for</tt>
-
# setting the appropriate set of tags that should be added to the layout.
-
#
-
# == Limitations
-
#
-
# Due to the rendering process limitation using <tt><%= yield_content %></tt>
-
# from within nested templates do not work above the <tt><%= yield %> statement.
-
# For more details https://github.com/sinatra/sinatra-contrib/issues/140#issuecomment-48831668
-
#
-
# # app.rb
-
# get '/' do
-
# erb :body, :layout => :layout do
-
# erb :foobar
-
# end
-
# end
-
#
-
# # foobar.erb
-
# <% content_for :one do %>
-
# <script>
-
# alert('one');
-
# </script>
-
# <% end %>
-
# <% content_for :two do %>
-
# <script>
-
# alert('two');
-
# </script>
-
# <% end %>
-
#
-
# Using <tt><%= yield_content %></tt> before <tt><%= yield %></tt> will cause only the second
-
# alert to display:
-
#
-
# # body.erb
-
# # Display only second alert
-
# <%= yield_content :one %>
-
# <%= yield %>
-
# <%= yield_content :two %>
-
#
-
# # body.erb
-
# # Display both alerts
-
# <%= yield %>
-
# <%= yield_content :one %>
-
# <%= yield_content :two %>
-
#
-
1
module ContentFor
-
1
include Capture
-
-
# Capture a block of content to be rendered later. For example:
-
#
-
# <% content_for :head do %>
-
# <script type="text/javascript" src="/foo.js"></script>
-
# <% end %>
-
#
-
# You can also pass an immediate value instead of a block:
-
#
-
# <% content_for :title, "foo" %>
-
#
-
# You can call +content_for+ multiple times with the same key
-
# (in the example +:head+), and when you render the blocks for
-
# that key all of them will be rendered, in the same order you
-
# captured them.
-
#
-
# Your blocks can also receive values, which are passed to them
-
# by <tt>yield_content</tt>
-
1
def content_for(key, value = nil, options = {}, &block)
-
block ||= proc { |*| value }
-
clear_content_for(key) if options[:flush]
-
content_blocks[key.to_sym] << capture_later(&block)
-
end
-
-
# Check if a block of content with the given key was defined. For
-
# example:
-
#
-
# <% content_for :head do %>
-
# <script type="text/javascript" src="/foo.js"></script>
-
# <% end %>
-
#
-
# <% if content_for? :head %>
-
# <span>content "head" was defined.</span>
-
# <% end %>
-
1
def content_for?(key)
-
content_blocks[key.to_sym].any?
-
end
-
-
# Unset a named block of content. For example:
-
#
-
# <% clear_content_for :head %>
-
1
def clear_content_for(key)
-
content_blocks.delete(key.to_sym) if content_for?(key)
-
end
-
-
# Render the captured blocks for a given key. For example:
-
#
-
# <head>
-
# <title>Example</title>
-
# <%= yield_content :head %>
-
# </head>
-
#
-
# Would render everything you declared with <tt>content_for
-
# :head</tt> before closing the <tt><head></tt> tag.
-
#
-
# You can also pass values to the content blocks by passing them
-
# as arguments after the key:
-
#
-
# <%= yield_content :head, 1, 2 %>
-
#
-
# Would pass <tt>1</tt> and <tt>2</tt> to all the blocks registered
-
# for <tt>:head</tt>.
-
1
def yield_content(key, *args, &block)
-
if block_given? && !content_for?(key)
-
(haml? && Tilt[:haml] == Tilt::HamlTemplate) ? capture_haml(*args, &block) : yield(*args)
-
else
-
content = content_blocks[key.to_sym].map { |b| capture(*args, &b) }
-
content.join.tap do |c|
-
if block_given? && (erb? || erubi? || erubis?)
-
@_out_buf << c
-
end
-
end
-
end
-
end
-
-
1
private
-
-
1
def content_blocks
-
@content_blocks ||= Hash.new { |h, k| h[k] = [] }
-
end
-
end
-
-
1
helpers ContentFor
-
end
-
1
require 'sinatra/contrib/setup'
-
-
1
module Sinatra
-
1
module Contrib
-
##
-
# Common middleware that doesn't bring run time overhead if not used
-
# or breaks if external dependencies are missing. Will extend
-
# Sinatra::Application by default.
-
1
module Common
-
1
register :ConfigFile
-
1
register :MultiRoute
-
1
register :Namespace
-
1
register :RespondWith
-
-
1
helpers :Capture
-
1
helpers :ContentFor
-
1
helpers :Cookies
-
1
helpers :EngineTracking
-
1
helpers :JSON
-
1
helpers :LinkHeader
-
1
helpers :Streaming
-
1
helpers :RequiredParams
-
end
-
-
##
-
# Other extensions you don't want to be loaded unless needed.
-
1
module Custom
-
# register :Compass
-
1
register :Reloader
-
end
-
-
##
-
# Stuff that aren't Sinatra extensions, technically.
-
1
autoload :Extension
-
1
autoload :TestHelpers
-
end
-
-
1
register Sinatra::Contrib::Common
-
end
-
1
require 'sinatra/base'
-
1
require 'sinatra/contrib/version'
-
1
require 'backports/rails/string' # for String#underscore
-
-
1
module Sinatra
-
1
module Contrib
-
1
module Loader
-
1
def extensions
-
13
@extensions ||= {:helpers => [], :register => []}
-
end
-
-
1
def register(name, path = nil)
-
5
autoload name, path, :register
-
end
-
-
1
def helpers(name, path = nil)
-
8
autoload name, path, :helpers
-
end
-
-
1
def autoload(name, path = nil, method = nil)
-
15
path ||= "sinatra/#{name.to_s.underscore}"
-
15
extensions[method] << name if method
-
15
Sinatra.autoload(name, path)
-
end
-
-
1
def registered(base)
-
3
@extensions.each do |method, list|
-
31
list = list.map { |name| Sinatra.const_get name }
-
6
base.send(method, *list) unless base == ::Sinatra::Application
-
end
-
end
-
end
-
-
1
module Common
-
1
extend Loader
-
end
-
-
1
module Custom
-
1
extend Loader
-
end
-
-
1
module All
-
1
def self.registered(base)
-
base.register Common, Custom
-
end
-
end
-
-
1
extend Loader
-
1
def self.registered(base)
-
1
base.register Common, Custom
-
end
-
end
-
end
-
1
module Sinatra
-
1
module Contrib
-
1
VERSION = '2.0.8.1'
-
end
-
end
-
-
1
require 'sinatra/base'
-
-
1
module Sinatra
-
# = Sinatra::Cookies
-
#
-
# Easy way to deal with cookies
-
#
-
# == Usage
-
#
-
# Allows you to read cookies:
-
#
-
# get '/' do
-
# "value: #{cookies[:something]}"
-
# end
-
#
-
# And of course to write cookies:
-
#
-
# get '/set' do
-
# cookies[:something] = 'foobar'
-
# redirect to('/')
-
# end
-
#
-
# And generally behaves like a hash:
-
#
-
# get '/demo' do
-
# cookies.merge! 'foo' => 'bar', 'bar' => 'baz'
-
# cookies.keep_if { |key, value| key.start_with? 'b' }
-
# foo, bar = cookies.values_at 'foo', 'bar'
-
# "size: #{cookies.length}"
-
# end
-
#
-
# === Classic Application
-
#
-
# In a classic application simply require the helpers, and start using them:
-
#
-
# require "sinatra"
-
# require "sinatra/cookies"
-
#
-
# # The rest of your classic application code goes here...
-
#
-
# === Modular Application
-
#
-
# In a modular application you need to require the helpers, and then tell
-
# the application to use them:
-
#
-
# require "sinatra/base"
-
# require "sinatra/cookies"
-
#
-
# class MyApp < Sinatra::Base
-
# helpers Sinatra::Cookies
-
#
-
# # The rest of your modular application code goes here...
-
# end
-
#
-
1
module Cookies
-
1
class Jar
-
1
include Enumerable
-
1
attr_reader :options
-
-
1
def initialize(app)
-
@response_string = nil
-
@response_hash = {}
-
@response = app.response
-
@request = app.request
-
@deleted = []
-
-
@options = {
-
:path => @request.script_name.to_s.empty? ? '/' : @request.script_name,
-
:domain => @request.host == 'localhost' ? nil : @request.host,
-
:secure => @request.secure?,
-
:httponly => true
-
}
-
-
if app.settings.respond_to? :cookie_options
-
@options.merge! app.settings.cookie_options
-
end
-
end
-
-
1
def ==(other)
-
other.respond_to? :to_hash and to_hash == other.to_hash
-
end
-
-
1
def [](key)
-
response_cookies[key.to_s] || request_cookies[key.to_s]
-
end
-
-
1
def []=(key, value)
-
set(key, value: value)
-
end
-
-
def assoc(key)
-
to_hash.assoc(key.to_s)
-
1
end if Hash.method_defined? :assoc
-
-
1
def clear
-
each_key { |k| delete(k) }
-
end
-
-
1
def compare_by_identity?
-
false
-
end
-
-
1
def default
-
nil
-
end
-
-
1
alias default_proc default
-
-
1
def delete(key)
-
result = self[key]
-
@response.delete_cookie(key.to_s, @options)
-
result
-
end
-
-
1
def delete_if
-
return enum_for(__method__) unless block_given?
-
each { |k, v| delete(k) if yield(k, v) }
-
self
-
end
-
-
1
def each(&block)
-
return enum_for(__method__) unless block_given?
-
to_hash.each(&block)
-
end
-
-
1
def each_key(&block)
-
return enum_for(__method__) unless block_given?
-
to_hash.each_key(&block)
-
end
-
-
1
alias each_pair each
-
-
1
def each_value(&block)
-
return enum_for(__method__) unless block_given?
-
to_hash.each_value(&block)
-
end
-
-
1
def empty?
-
to_hash.empty?
-
end
-
-
1
def fetch(key, &block)
-
response_cookies.fetch(key.to_s) do
-
request_cookies.fetch(key.to_s, &block)
-
end
-
end
-
-
def flatten
-
to_hash.flatten
-
1
end if Hash.method_defined? :flatten
-
-
1
def has_key?(key)
-
response_cookies.has_key? key.to_s or request_cookies.has_key? key.to_s
-
end
-
-
1
def has_value?(value)
-
response_cookies.has_value? value or request_cookies.has_value? value
-
end
-
-
1
def hash
-
to_hash.hash
-
end
-
-
1
alias include? has_key?
-
1
alias member? has_key?
-
-
1
def index(value)
-
warn "Hash#index is deprecated; use Hash#key"
-
key(value)
-
end
-
-
1
def inspect
-
"<##{self.class}: #{to_hash.inspect[1..-2]}>"
-
end
-
-
def invert
-
to_hash.invert
-
1
end if Hash.method_defined? :invert
-
-
1
def keep_if
-
return enum_for(__method__) unless block_given?
-
delete_if { |*a| not yield(*a) }
-
end
-
-
1
def key(value)
-
to_hash.key(value)
-
end
-
-
1
alias key? has_key?
-
-
1
def keys
-
to_hash.keys
-
end
-
-
1
def length
-
to_hash.length
-
end
-
-
1
def merge(other, &block)
-
to_hash.merge(other, &block)
-
end
-
-
1
def merge!(other)
-
other.each_pair do |key, value|
-
if block_given? and include? key
-
self[key] = yield(key.to_s, self[key], value)
-
else
-
self[key] = value
-
end
-
end
-
end
-
-
1
def rassoc(value)
-
to_hash.rassoc(value)
-
end
-
-
1
def rehash
-
response_cookies.rehash
-
request_cookies.rehash
-
self
-
end
-
-
1
def reject(&block)
-
return enum_for(__method__) unless block_given?
-
to_hash.reject(&block)
-
end
-
-
1
alias reject! delete_if
-
-
1
def replace(other)
-
select! { |k, v| other.include?(k) or other.include?(k.to_s) }
-
merge! other
-
end
-
-
1
def select(&block)
-
return enum_for(__method__) unless block_given?
-
to_hash.select(&block)
-
end
-
-
1
alias select! keep_if if Hash.method_defined? :select!
-
-
1
def set(key, options = {})
-
@response.set_cookie key.to_s, @options.merge(options)
-
end
-
-
1
def shift
-
key, value = to_hash.shift
-
delete(key)
-
[key, value]
-
end
-
-
1
alias size length
-
-
def sort(&block)
-
to_hash.sort(&block)
-
1
end if Hash.method_defined? :sort
-
-
1
alias store []=
-
-
1
def to_hash
-
request_cookies.merge(response_cookies)
-
end
-
-
1
def to_a
-
to_hash.to_a
-
end
-
-
1
def to_s
-
to_hash.to_s
-
end
-
-
1
alias update merge!
-
1
alias value? has_value?
-
-
1
def values
-
to_hash.values
-
end
-
-
1
def values_at(*list)
-
list.map { |k| self[k] }
-
end
-
-
1
private
-
-
1
def warn(message)
-
super "#{caller.first[/^[^:]:\d+:/]} warning: #{message}"
-
end
-
-
1
def deleted
-
parse_response
-
@deleted
-
end
-
-
1
def response_cookies
-
parse_response
-
@response_hash
-
end
-
-
1
def parse_response
-
string = @response['Set-Cookie']
-
return if @response_string == string
-
-
hash = {}
-
-
string.each_line do |line|
-
key, value = line.split(';', 2).first.to_s.split('=', 2)
-
next if key.nil?
-
key = Rack::Utils.unescape(key)
-
if line =~ /expires=Thu, 01[-\s]Jan[-\s]1970/
-
@deleted << key
-
else
-
@deleted.delete key
-
hash[key] = value
-
end
-
end
-
-
@response_hash.replace hash
-
@response_string = string
-
end
-
-
1
def request_cookies
-
@request.cookies.reject { |key, value| deleted.include? key }
-
end
-
end
-
-
1
def cookies
-
@cookies ||= Jar.new(self)
-
end
-
end
-
-
1
helpers Cookies
-
end
-
1
require 'sinatra/base'
-
1
require 'multi_json'
-
1
module Sinatra
-
-
# = Sinatra::JSON
-
#
-
# <tt>Sinatra::JSON</tt> adds a helper method, called +json+, for (obviously)
-
# json generation.
-
#
-
# == Usage
-
#
-
# === Classic Application
-
#
-
# In a classic application simply require the helper, and start using it:
-
#
-
# require "sinatra"
-
# require "sinatra/json"
-
#
-
# # define a route that uses the helper
-
# get '/' do
-
# json :foo => 'bar'
-
# end
-
#
-
# # The rest of your classic application code goes here...
-
#
-
# === Modular Application
-
#
-
# In a modular application you need to require the helper, and then tell the
-
# application you will use it:
-
#
-
# require "sinatra/base"
-
# require "sinatra/json"
-
#
-
# class MyApp < Sinatra::Base
-
#
-
# # define a route that uses the helper
-
# get '/' do
-
# json :foo => 'bar'
-
# end
-
#
-
# # The rest of your modular application code goes here...
-
# end
-
#
-
# === Encoders
-
#
-
# By default it will try to call +to_json+ on the object, but if it doesn't
-
# respond to that message, it will use its own rather simple encoder. You can
-
# easily change that anyways. To use +JSON+, simply require it:
-
#
-
# require 'json'
-
#
-
# The same goes for <tt>Yajl::Encoder</tt>:
-
#
-
# require 'yajl'
-
#
-
# For other encoders, besides requiring them, you need to define the
-
# <tt>:json_encoder</tt> setting. For instance, for the +Whatever+ encoder:
-
#
-
# require 'whatever'
-
# set :json_encoder, Whatever
-
#
-
# To force +json+ to simply call +to_json+ on the object:
-
#
-
# set :json_encoder, :to_json
-
#
-
# Actually, it can call any method:
-
#
-
# set :json_encoder, :my_fancy_json_method
-
#
-
# === Content-Type
-
#
-
# It will automatically set the content type to "application/json". As
-
# usual, you can easily change that, with the <tt>:json_content_type</tt>
-
# setting:
-
#
-
# set :json_content_type, :js
-
#
-
# === Overriding the Encoder and the Content-Type
-
#
-
# The +json+ helper will also take two options <tt>:encoder</tt> and
-
# <tt>:content_type</tt>. The values of this options are the same as the
-
# <tt>:json_encoder</tt> and <tt>:json_content_type</tt> settings,
-
# respectively. You can also pass those to the json method:
-
#
-
# get '/' do
-
# json({:foo => 'bar'}, :encoder => :to_json, :content_type => :js)
-
# end
-
#
-
1
module JSON
-
1
class << self
-
1
def encode(object)
-
::MultiJson.dump(object)
-
end
-
end
-
-
1
def json(object, options = {})
-
content_type resolve_content_type(options)
-
resolve_encoder_action object, resolve_encoder(options)
-
end
-
-
1
private
-
-
1
def resolve_content_type(options = {})
-
options[:content_type] || settings.json_content_type
-
end
-
-
1
def resolve_encoder(options = {})
-
options[:json_encoder] || settings.json_encoder
-
end
-
-
1
def resolve_encoder_action(object, encoder)
-
[:encode, :generate].each do |method|
-
return encoder.send(method, object) if encoder.respond_to? method
-
end
-
if encoder.is_a? Symbol
-
object.__send__(encoder)
-
else
-
fail "#{encoder} does not respond to #generate nor #encode"
-
end #if
-
end #resolve_encoder_action
-
end #JSON
-
-
1
Base.set :json_encoder do
-
::MultiJson
-
end
-
-
1
Base.set :json_content_type, :json
-
-
# Load the JSON helpers in modular style automatically
-
1
Base.helpers JSON
-
end
-
1
require 'sinatra/base'
-
-
1
module Sinatra
-
-
# = Sinatra::LinkHeader
-
#
-
# <tt>Sinatra::LinkHeader</tt> adds a set of helper methods to generate link
-
# HTML tags and their corresponding Link HTTP headers.
-
#
-
# == Usage
-
#
-
# Once you had set up the helpers in your application (see below), you will
-
# be able to call the following methods from inside your route handlers,
-
# filters and templates:
-
#
-
# +prefetch+::
-
# Sets the Link HTTP headers and returns HTML tags to prefetch the given
-
# resources.
-
#
-
# +stylesheet+::
-
# Sets the Link HTTP headers and returns HTML tags to use the given
-
# stylesheets.
-
#
-
# +link+::
-
# Sets the Link HTTP headers and returns the corresponding HTML tags
-
# for the given resources.
-
#
-
# +link_headers+::
-
# Returns the corresponding HTML tags for the current Link HTTP headers.
-
#
-
# === Classic Application
-
#
-
# In a classic application simply require the helpers, and start using them:
-
#
-
# require "sinatra"
-
# require "sinatra/link_header"
-
#
-
# # The rest of your classic application code goes here...
-
#
-
# === Modular Application
-
#
-
# In a modular application you need to require the helpers, and then tell
-
# the application you will use them:
-
#
-
# require "sinatra/base"
-
# require "sinatra/link_header"
-
#
-
# class MyApp < Sinatra::Base
-
# helpers Sinatra::LinkHeader
-
#
-
# # The rest of your modular application code goes here...
-
# end
-
#
-
1
module LinkHeader
-
##
-
# Sets Link HTTP header and returns HTML tags for telling the browser to
-
# prefetch given resources (only supported by Opera and Firefox at the
-
# moment).
-
1
def prefetch(*urls)
-
link(:prefetch, *urls)
-
end
-
-
##
-
# Sets Link HTTP header and returns HTML tags for using stylesheets.
-
1
def stylesheet(*urls)
-
urls << {} unless urls.last.respond_to? :to_hash
-
urls.last[:type] ||= mime_type(:css)
-
link(:stylesheet, *urls)
-
end
-
-
##
-
# Sets Link HTTP header and returns corresponding HTML tags.
-
#
-
# Example:
-
#
-
# # Sets header:
-
# # Link: </foo>; rel="next"
-
# # Returns String:
-
# # '<link href="/foo" rel="next" />'
-
# link '/foo', :rel => :next
-
#
-
# # Multiple URLs
-
# link :stylesheet, '/a.css', '/b.css'
-
1
def link(*urls)
-
opts = urls.last.respond_to?(:to_hash) ? urls.pop : {}
-
opts[:rel] = urls.shift unless urls.first.respond_to? :to_str
-
options = opts.map { |k, v| " #{k}=#{v.to_s.inspect}" }
-
html_pattern = "<link href=\"%s\"#{options.join} />"
-
http_pattern = ["<%s>", *options].join ";"
-
link = (response["Link"] ||= "")
-
-
urls.map do |url|
-
link << ",\n" unless link.empty?
-
link << (http_pattern % url)
-
html_pattern % url
-
end.join "\n"
-
end
-
-
##
-
# Takes the current value of th Link header(s) and generates HTML tags
-
# from it.
-
#
-
# Example:
-
#
-
# get '/' do
-
# # You can of course use fancy helpers like #link, #stylesheet
-
# # or #prefetch
-
# response["Link"] = '</foo>; rel="next"'
-
# haml :some_page
-
# end
-
#
-
# __END__
-
#
-
# @@ layout
-
# %head= link_headers
-
# %body= yield
-
1
def link_headers
-
yield if block_given?
-
return "" unless response.include? "Link"
-
response["Link"].split(",\n").map do |line|
-
url, *opts = line.split(';').map(&:strip)
-
"<link href=\"#{url[1..-2]}\" #{opts.join " "} />"
-
end.join "\n"
-
end
-
-
1
def self.registered(base)
-
puts "WARNING: #{self} is a helpers module, not an extension."
-
end
-
end
-
-
1
helpers LinkHeader
-
end
-
1
require 'sinatra/base'
-
-
1
module Sinatra
-
# = Sinatra::MultiRoute
-
#
-
# Create multiple routes with one statement.
-
#
-
# == Usage
-
#
-
# Use this extension to create a handler for multiple routes:
-
#
-
# get '/foo', '/bar' do
-
# # ...
-
# end
-
#
-
# Or for multiple verbs:
-
#
-
# route :get, :post, '/' do
-
# # ...
-
# end
-
#
-
# Or for multiple verbs and multiple routes:
-
#
-
# route :get, :post, ['/foo', '/bar'] do
-
# # ...
-
# end
-
#
-
# Or even for custom verbs:
-
#
-
# route 'LIST', '/' do
-
# # ...
-
# end
-
#
-
# === Classic Application
-
#
-
# To use the extension in a classic application all you need to do is require
-
# it:
-
#
-
# require "sinatra"
-
# require "sinatra/multi_route"
-
#
-
# # Your classic application code goes here...
-
#
-
# === Modular Application
-
#
-
# To use the extension in a modular application you need to require it, and
-
# then, tell the application you will use it:
-
#
-
# require "sinatra/base"
-
# require "sinatra/multi_route"
-
#
-
# class MyApp < Sinatra::Base
-
# register Sinatra::MultiRoute
-
#
-
# # The rest of your modular application code goes here...
-
# end
-
#
-
1
module MultiRoute
-
1
def head(*args, &block) super(*route_args(args), &block) end
-
1
def delete(*args, &block) super(*route_args(args), &block) end
-
7
def get(*args, &block) super(*route_args(args), &block) end
-
1
def options(*args, &block) super(*route_args(args), &block) end
-
1
def patch(*args, &block) super(*route_args(args), &block) end
-
1
def post(*args, &block) super(*route_args(args), &block) end
-
1
def put(*args, &block) super(*route_args(args), &block) end
-
-
1
def route(*args, &block)
-
12
options = Hash === args.last ? args.pop : {}
-
12
routes = [*args.pop]
-
12
args.each do |verb|
-
12
verb = verb.to_s.upcase if Symbol === verb
-
12
routes.each do |route|
-
12
super(verb, route, options, &block)
-
end
-
end
-
end
-
-
1
private
-
-
1
def route_args(args)
-
6
options = Hash === args.last ? args.pop : {}
-
6
[args, options]
-
end
-
end
-
-
1
register MultiRoute
-
end
-
1
require 'sinatra/base'
-
1
require 'mustermann'
-
-
1
module Sinatra
-
-
# = Sinatra::Namespace
-
#
-
# <tt>Sinatra::Namespace</tt> is an extension that adds namespaces to an
-
# application. This namespaces will allow you to share a path prefix for the
-
# routes within the namespace, and define filters, conditions and error
-
# handlers exclusively for them. Besides that, you can also register helpers
-
# and extensions that will be used only within the namespace.
-
#
-
# == Usage
-
#
-
# Once you have loaded the extension (see below), you can use the +namespace+
-
# method to define namespaces in your application.
-
#
-
# You can define a namespace by a path prefix:
-
#
-
# namespace '/blog' do
-
# get { haml :blog }
-
# get '/:entry_permalink' do
-
# @entry = Entry.find_by_permalink!(params[:entry_permalink])
-
# haml :entry
-
# end
-
#
-
# # More blog routes...
-
# end
-
#
-
# by a condition:
-
#
-
# namespace :host_name => 'localhost' do
-
# get('/admin/dashboard') { haml :dashboard }
-
# get('/admin/login') { haml :login }
-
#
-
# # More admin routes...
-
# end
-
#
-
# or both:
-
#
-
# namespace '/admin', :host_name => 'localhost' do
-
# get('/dashboard') { haml :dashboard }
-
# get('/login') { haml :login }
-
# post('/login') { login_user }
-
#
-
# # More admin routes...
-
# end
-
#
-
# Regex is also accepted:
-
#
-
# namespace /\/posts\/([^\/&?]+)\// do
-
# get { haml :blog }
-
#
-
# # More blog routes...
-
# end
-
#
-
# When you define a filter or an error handler, or register an extension or a
-
# set of helpers within a namespace, they only affect the routes defined in
-
# it. For instance, lets define a before filter to prevent the access of
-
# unauthorized users to the admin section of the application:
-
#
-
# namespace '/admin' do
-
# helpers AdminHelpers
-
# before { authenticate unless request.path_info == '/admin/login' }
-
#
-
# get '/dashboard' do
-
# # Only authenticated users can access here...
-
# haml :dashboard
-
# end
-
#
-
# # More admin routes...
-
# end
-
#
-
# get '/' do
-
# # Any user can access here...
-
# haml :index
-
# end
-
#
-
# Well, they actually also affect the nested namespaces:
-
#
-
# namespace '/admin' do
-
# helpers AdminHelpers
-
# before { authenticate unless request.path_info == '/admin/login' }
-
#
-
# namespace '/users' do
-
# get do
-
# # Only authenticated users can access here...
-
# @users = User.all
-
# haml :users
-
# end
-
#
-
# # More user admin routes...
-
# end
-
#
-
# # More admin routes...
-
# end
-
#
-
# Redirecting within the namespace can be done using redirect_to:
-
#
-
# namespace '/admin' do
-
# get '/foo' do
-
# redirect_to '/bar' # Redirects to /admin/bar
-
# end
-
#
-
# get '/foo' do
-
# redirect '/bar' # Redirects to /bar
-
# end
-
# end
-
#
-
# === Classic Application Setup
-
#
-
# To be able to use namespaces in a classic application all you need to do is
-
# require the extension:
-
#
-
# require "sinatra"
-
# require "sinatra/namespace"
-
#
-
# namespace '/users' do
-
# end
-
#
-
# === Modular Application Setup
-
#
-
# To be able to use namespaces in a modular application all you need to do is
-
# require the extension, and then, register it:
-
#
-
# require "sinatra/base"
-
# require "sinatra/namespace"
-
#
-
# class MyApp < Sinatra::Base
-
# register Sinatra::Namespace
-
#
-
# namespace '/users' do
-
# end
-
# end
-
#
-
# === Within an extension
-
#
-
# To be able to use namespaces within an extension, you need to first create
-
# an extension. This includes defining the `registered(app)` method in the
-
# module.
-
#
-
# require 'sinatra/base' # For creating Sinatra extensions
-
# require 'sinatra/namespace' # To create namespaces
-
#
-
# module Zomg # Keep everything under "Zomg" namespace for sanity
-
# module Routes # Define a new "Routes" module
-
#
-
# def self.registered(app)
-
# # First, register the Namespace extension
-
# app.register Sinatra::Namespace
-
#
-
# # This defines an `/api` namespace on the application
-
# app.namespace '/api' do
-
# get '/users' do
-
# # do something with `GET "/api/users"`
-
# end
-
# end
-
#
-
# end
-
# end
-
#
-
# # Lastly, register the extension to use in our app
-
# Sinatra.register Routes
-
# end
-
#
-
# In order to use this extension, is the same as any other Sinatra extension:
-
#
-
# module Zomg
-
# # Define our app class, we use modular app for this example
-
# class App < Sinatra::Base
-
# # this gives us all the namespaces we defined earlier
-
# register Routes
-
#
-
# get '/' do
-
# "Welcome to my application!"
-
# end
-
# end
-
# end
-
#
-
# Zomg::App.run! # Don't forget to start your app ;)
-
#
-
# Phew! That was a mouthful.
-
#
-
# I hope that helps you use `Sinatra::Namespace` in every way imaginable!
-
#
-
1
module Namespace
-
1
def self.new(base, pattern, conditions = {}, &block)
-
Module.new do
-
#quelch uninitialized variable warnings, since these get used by compile method.
-
@pattern, @conditions = nil, nil
-
extend NamespacedMethods
-
include InstanceMethods
-
@base, @extensions, @errors = base, [], {}
-
@pattern, @conditions = compile(pattern, conditions)
-
@templates = Hash.new { |h,k| @base.templates[k] }
-
namespace = self
-
before { extend(@namespace = namespace) }
-
class_eval(&block)
-
end
-
end
-
-
1
module InstanceMethods
-
1
def settings
-
@namespace
-
end
-
-
1
def template_cache
-
super.fetch(:nested, @namespace) { Tilt::Cache.new }
-
end
-
-
1
def redirect_to(uri, *args)
-
redirect("#{@namespace.pattern}#{uri}", *args)
-
end
-
end
-
-
1
module SharedMethods
-
1
def namespace(pattern, conditions = {}, &block)
-
Sinatra::Namespace.new(self, pattern, conditions, &block)
-
end
-
end
-
-
1
module NamespacedMethods
-
1
include SharedMethods
-
1
attr_reader :base, :templates
-
-
1
def self.prefixed(*names)
-
10
names.each { |n| define_method(n) { |*a, &b| prefixed(n, *a, &b) }}
-
end
-
-
1
prefixed :before, :after, :delete, :get, :head, :options, :patch, :post, :put
-
-
1
def helpers(*extensions, &block)
-
class_eval(&block) if block_given?
-
include(*extensions) if extensions.any?
-
end
-
-
1
def register(*extensions, &block)
-
extensions << Module.new(&block) if block_given?
-
@extensions += extensions
-
extensions.each do |extension|
-
extend extension
-
extension.registered(self) if extension.respond_to?(:registered)
-
end
-
end
-
-
1
def invoke_hook(name, *args)
-
@extensions.each { |e| e.send(name, *args) if e.respond_to?(name) }
-
end
-
-
1
def not_found(&block)
-
error(Sinatra::NotFound, &block)
-
end
-
-
1
def errors
-
base.errors.merge(namespace_errors)
-
end
-
-
1
def namespace_errors
-
@errors
-
end
-
-
1
def error(*codes, &block)
-
args = Sinatra::Base.send(:compile!, "ERROR", /.*/, block)
-
codes = codes.map { |c| Array(c) }.flatten
-
codes << Exception if codes.empty?
-
codes << Sinatra::NotFound if codes.include?(404)
-
-
codes.each do |c|
-
errors = @errors[c] ||= []
-
errors << args
-
end
-
end
-
-
1
def respond_to(*args)
-
return @conditions[:provides] || base.respond_to if args.empty?
-
@conditions[:provides] = args
-
end
-
-
1
def set(key, value = self, &block)
-
raise ArgumentError, "may not set #{key}" if key != :views
-
return key.each { |k,v| set(k, v) } if block.nil? and value == self
-
block ||= proc { value }
-
singleton_class.send(:define_method, key, &block)
-
end
-
-
1
def enable(*opts)
-
opts.each { |key| set(key, true) }
-
end
-
-
1
def disable(*opts)
-
opts.each { |key| set(key, false) }
-
end
-
-
1
def template(name, &block)
-
filename, line = caller_locations.first
-
templates[name] = [block, filename, line.to_i]
-
end
-
-
1
def layout(name=:layout, &block)
-
template name, &block
-
end
-
-
1
def pattern
-
@pattern
-
end
-
-
1
private
-
-
1
def app
-
base.respond_to?(:base) ? base.base : base
-
end
-
-
1
def compile(pattern, conditions, default_pattern = nil)
-
if pattern.respond_to? :to_hash
-
conditions = conditions.merge pattern.to_hash
-
pattern = nil
-
end
-
base_pattern, base_conditions = @pattern, @conditions
-
pattern ||= default_pattern
-
[ prefixed_path(base_pattern, pattern),
-
(base_conditions || {}).merge(conditions) ]
-
end
-
-
1
def prefixed_path(a, b)
-
return a || b || /.*/ unless a and b
-
return Mustermann.new(b) if a == /.*/
-
-
Mustermann.new(a) + Mustermann.new(b)
-
end
-
-
1
def prefixed(method, pattern = nil, conditions = {}, &block)
-
default = %r{(?:/.*)?} if method == :before or method == :after
-
pattern, conditions = compile pattern, conditions, default
-
result = base.send(method, pattern, conditions, &block)
-
invoke_hook :route_added, method.to_s.upcase, pattern, block
-
result
-
end
-
-
1
def method_missing(method, *args, &block)
-
base.send(method, *args, &block)
-
end
-
-
1
def respond_to?(method, include_private = false)
-
super || base.respond_to?(method, include_private)
-
end
-
end
-
-
1
module BaseMethods
-
1
include SharedMethods
-
end
-
-
1
def self.extended(base)
-
2
base.extend BaseMethods
-
end
-
end
-
-
1
register Sinatra::Namespace
-
1
Delegator.delegate :namespace
-
end
-
1
require 'sinatra/base'
-
-
1
module Sinatra
-
-
# = Sinatra::Reloader
-
#
-
# Extension to reload modified files. Useful during development,
-
# since it will automatically require files defining routes, filters,
-
# error handlers and inline templates, with every incoming request,
-
# but only if they have been updated.
-
#
-
# == Usage
-
#
-
# === Classic Application
-
#
-
# To enable the reloader in a classic application all you need to do is
-
# require it:
-
#
-
# require "sinatra"
-
# require "sinatra/reloader" if development?
-
#
-
# # Your classic application code goes here...
-
#
-
# === Modular Application
-
#
-
# To enable the reloader in a modular application all you need to do is
-
# require it, and then, register it:
-
#
-
# require "sinatra/base"
-
# require "sinatra/reloader"
-
#
-
# class MyApp < Sinatra::Base
-
# configure :development do
-
# register Sinatra::Reloader
-
# end
-
#
-
# # Your modular application code goes here...
-
# end
-
#
-
# == Using the Reloader in Other Environments
-
#
-
# By default, the reloader is only enabled for the development
-
# environment. Similar to registering the reloader in a modular
-
# application, a classic application requires manually enabling the
-
# extension for it to be available in a non-development environment.
-
#
-
# require "sinatra"
-
# require "sinatra/reloader"
-
#
-
# configure :production do
-
# enable :reloader
-
# end
-
#
-
# == Changing the Reloading Policy
-
#
-
# You can refine the reloading policy with +also_reload+ and
-
# +dont_reload+, to customize which files should, and should not, be
-
# reloaded, respectively. You can also use +after_reload+ to execute a
-
# block after any file being reloaded.
-
#
-
# === Classic Application
-
#
-
# Simply call the methods:
-
#
-
# require "sinatra"
-
# require "sinatra/reloader" if development?
-
#
-
# also_reload '/path/to/some/file'
-
# dont_reload '/path/to/other/file'
-
# after_reload do
-
# puts 'reloaded'
-
# end
-
#
-
# # Your classic application code goes here...
-
#
-
# === Modular Application
-
#
-
# Call the methods inside the +configure+ block:
-
#
-
# require "sinatra/base"
-
# require "sinatra/reloader"
-
#
-
# class MyApp < Sinatra::Base
-
# configure :development do
-
# register Sinatra::Reloader
-
# also_reload '/path/to/some/file'
-
# dont_reload '/path/to/other/file'
-
# after_reload do
-
# puts 'reloaded'
-
# end
-
# end
-
#
-
# # Your modular application code goes here...
-
# end
-
#
-
1
module Reloader
-
-
# Watches a file so it can tell when it has been updated, and what
-
# elements does it contain.
-
1
class Watcher
-
-
# Represents an element of a Sinatra application that may need to
-
# be reloaded. An element could be:
-
# * a route
-
# * a filter
-
# * an error handler
-
# * a middleware
-
# * inline templates
-
#
-
# Its +representation+ attribute is there to allow to identify the
-
# element within an application, that is, to match it with its
-
# Sinatra's internal representation.
-
1
class Element < Struct.new(:type, :representation)
-
end
-
-
# Collection of file +Watcher+ that can be associated with a
-
# Sinatra application. That way, we can know which files belong
-
# to a given application and which files have been modified. It
-
# also provides a mechanism to inform a Watcher of the elements
-
# defined in the file being watched and if its changes should be
-
# ignored.
-
1
class List
-
4
@app_list_map = Hash.new { |hash, key| hash[key] = new }
-
-
# Returns the +List+ for the application +app+.
-
1
def self.for(app)
-
20
@app_list_map[app]
-
end
-
-
# Creates a new +List+ instance.
-
1
def initialize
-
3
@path_watcher_map = Hash.new do |hash, key|
-
6
hash[key] = Watcher.new(key)
-
end
-
end
-
-
# Lets the +Watcher+ for the file located at +path+ know that the
-
# +element+ is defined there, and adds the +Watcher+ to the +List+,
-
# if it isn't already there.
-
1
def watch(path, element)
-
22
watcher_for(path).elements << element
-
end
-
-
# Tells the +Watcher+ for the file located at +path+ to ignore
-
# the file changes, and adds the +Watcher+ to the +List+, if
-
# it isn't already there.
-
1
def ignore(path)
-
watcher_for(path).ignore
-
end
-
-
# Adds a +Watcher+ for the file located at +path+ to the
-
# +List+, if it isn't already there.
-
1
def watcher_for(path)
-
22
@path_watcher_map[File.expand_path(path)]
-
end
-
1
alias watch_file watcher_for
-
-
# Returns an array with all the watchers in the +List+.
-
1
def watchers
-
@path_watcher_map.values
-
end
-
-
# Returns an array with all the watchers in the +List+ that
-
# have been updated.
-
1
def updated
-
watchers.find_all(&:updated?)
-
end
-
end
-
-
1
attr_reader :path, :elements, :mtime
-
-
# Creates a new +Watcher+ instance for the file located at +path+.
-
1
def initialize(path)
-
6
@ignore = nil
-
6
@path, @elements = path, []
-
6
update
-
end
-
-
# Indicates whether or not the file being watched has been modified.
-
1
def updated?
-
!ignore? && !removed? && mtime != File.mtime(path)
-
end
-
-
# Updates the mtime of the file being watched.
-
1
def update
-
6
@mtime = File.mtime(path)
-
end
-
-
# Indicates whether or not the file being watched has inline
-
# templates.
-
1
def inline_templates?
-
elements.any? { |element| element.type == :inline_templates }
-
end
-
-
# Informs that the modifications to the file being watched
-
# should be ignored.
-
1
def ignore
-
@ignore = true
-
end
-
-
# Indicates whether or not the modifications to the file being
-
# watched should be ignored.
-
1
def ignore?
-
!!@ignore
-
end
-
-
# Indicates whether or not the file being watched has been removed.
-
1
def removed?
-
!File.exist?(path)
-
end
-
end
-
-
1
MUTEX_FOR_PERFORM = Mutex.new
-
-
# Allow a block to be executed after any file being reloaded
-
1
@@after_reload = []
-
1
def after_reload(&block)
-
@@after_reload << block
-
end
-
-
# When the extension is registered it extends the Sinatra application
-
# +klass+ with the modules +BaseMethods+ and +ExtensionMethods+ and
-
# defines a before filter to +perform+ the reload of the modified files.
-
1
def self.registered(klass)
-
3
@reloader_loaded_in ||= {}
-
3
return if @reloader_loaded_in[klass]
-
-
3
@reloader_loaded_in[klass] = true
-
-
3
klass.extend BaseMethods
-
3
klass.extend ExtensionMethods
-
9
klass.set(:reloader) { klass.development? }
-
5
klass.set(:reload_templates) { klass.reloader? }
-
3
klass.before do
-
4
if klass.reloader?
-
MUTEX_FOR_PERFORM.synchronize { Reloader.perform(klass) }
-
end
-
end
-
3
klass.set(:inline_templates, klass.app_file) if klass == Sinatra::Application
-
end
-
-
# Reloads the modified files, adding, updating and removing the
-
# needed elements.
-
1
def self.perform(klass)
-
Watcher::List.for(klass).updated.each do |watcher|
-
klass.set(:inline_templates, watcher.path) if watcher.inline_templates?
-
watcher.elements.each { |element| klass.deactivate(element) }
-
$LOADED_FEATURES.delete(watcher.path)
-
require watcher.path
-
watcher.update
-
end
-
@@after_reload.each(&:call)
-
end
-
-
# Contains the methods defined in Sinatra::Base that are overridden.
-
1
module BaseMethods
-
# Protects Sinatra::Base.run! from being called more than once.
-
1
def run!(*args)
-
if settings.reloader?
-
super unless running?
-
else
-
super
-
end
-
end
-
-
# Does everything Sinatra::Base#route does, but it also tells the
-
# +Watcher::List+ for the Sinatra application to watch the defined
-
# route.
-
#
-
# Note: We are using #compile! so we don't interfere with extensions
-
# changing #route.
-
1
def compile!(verb, path, block, options = {})
-
16
source_location = block.respond_to?(:source_location) ?
-
block.source_location.first : caller_files[1]
-
16
signature = super
-
16
watch_element(
-
source_location, :route, { :verb => verb, :signature => signature }
-
)
-
16
signature
-
end
-
-
# Does everything Sinatra::Base#inline_templates= does, but it also
-
# tells the +Watcher::List+ for the Sinatra application to watch the
-
# inline templates in +file+ or the file who made the call to this
-
# method.
-
1
def inline_templates=(file=nil)
-
1
file = (file.nil? || file == true) ?
-
1
(caller_files[1] || File.expand_path($0)) : file
-
1
watch_element(file, :inline_templates)
-
1
super
-
end
-
-
# Does everything Sinatra::Base#use does, but it also tells the
-
# +Watcher::List+ for the Sinatra application to watch the middleware
-
# being used.
-
1
def use(middleware, *args, &block)
-
path = caller_files[1] || File.expand_path($0)
-
watch_element(path, :middleware, [middleware, args, block])
-
super
-
end
-
-
# Does everything Sinatra::Base#add_filter does, but it also tells
-
# the +Watcher::List+ for the Sinatra application to watch the defined
-
# filter.
-
1
def add_filter(type, path = nil, options = {}, &block)
-
3
source_location = block.respond_to?(:source_location) ?
-
block.source_location.first : caller_files[1]
-
3
result = super
-
3
watch_element(source_location, :"#{type}_filter", filters[type].last)
-
3
result
-
end
-
-
# Does everything Sinatra::Base#error does, but it also tells the
-
# +Watcher::List+ for the Sinatra application to watch the defined
-
# error handler.
-
1
def error(*codes, &block)
-
1
path = caller_files[1] || File.expand_path($0)
-
1
result = super
-
1
codes.each do |c|
-
watch_element(path, :error, :code => c, :handler => @errors[c])
-
end
-
1
result
-
end
-
-
# Does everything Sinatra::Base#register does, but it also lets the
-
# reloader know that an extension is being registered, because the
-
# elements defined in its +registered+ method need a special treatment.
-
1
def register(*extensions, &block)
-
1
start_registering_extension
-
1
result = super
-
1
stop_registering_extension
-
1
result
-
end
-
-
# Does everything Sinatra::Base#register does and then registers the
-
# reloader in the +subclass+.
-
1
def inherited(subclass)
-
1
result = super
-
1
subclass.register Sinatra::Reloader
-
1
result
-
end
-
end
-
-
# Contains the methods that the extension adds to the Sinatra application.
-
1
module ExtensionMethods
-
# Removes the +element+ from the Sinatra application.
-
1
def deactivate(element)
-
case element.type
-
when :route then
-
verb = element.representation[:verb]
-
signature = element.representation[:signature]
-
(routes[verb] ||= []).delete(signature)
-
when :middleware then
-
@middleware.delete(element.representation)
-
when :before_filter then
-
filters[:before].delete(element.representation)
-
when :after_filter then
-
filters[:after].delete(element.representation)
-
when :error then
-
code = element.representation[:code]
-
handler = element.representation[:handler]
-
@errors.delete(code) if @errors[code] == handler
-
end
-
end
-
-
# Indicates with a +glob+ which files should be reloaded if they
-
# have been modified. It can be called several times.
-
1
def also_reload(*glob)
-
Dir[*glob].each { |path| Watcher::List.for(self).watch_file(path) }
-
end
-
-
# Indicates with a +glob+ which files should not be reloaded even if
-
# they have been modified. It can be called several times.
-
1
def dont_reload(*glob)
-
Dir[*glob].each { |path| Watcher::List.for(self).ignore(path) }
-
end
-
-
1
private
-
-
# attr_reader :register_path warn on -w (private attribute)
-
23
def register_path; @register_path ||= nil; end
-
-
# Indicates an extesion is being registered.
-
1
def start_registering_extension
-
1
@register_path = caller_files[2]
-
end
-
-
# Indicates the extesion has already been registered.
-
1
def stop_registering_extension
-
1
@register_path = nil
-
end
-
-
# Indicates whether or not an extension is being registered.
-
1
def registering_extension?
-
20
!register_path.nil?
-
end
-
-
# Builds a Watcher::Element from +type+ and +representation+ and
-
# tells the Watcher::List for the current application to watch it
-
# in the file located at +path+.
-
#
-
# If an extension is being registered, it also tells the list to
-
# watch it in the file where the extension has been registered.
-
# This prevents the duplication of the elements added by the
-
# extension in its +registered+ method with every reload.
-
1
def watch_element(path, type, representation=nil)
-
20
list = Watcher::List.for(self)
-
20
element = Watcher::Element.new(type, representation)
-
20
list.watch(path, element)
-
20
list.watch(register_path, element) if registering_extension?
-
end
-
end
-
end
-
-
1
register Reloader
-
1
Delegator.delegate :also_reload, :dont_reload
-
end
-
1
require 'sinatra/base'
-
-
1
module Sinatra
-
# = Sinatra::RequiredParams
-
#
-
# Ensure required query parameters
-
#
-
# == Usage
-
#
-
# Set required query parameter keys in the argument.
-
# It'll halt with 400 if required keys don't exist.
-
#
-
# get '/simple_keys' do
-
# required_params :p1, :p2
-
# end
-
#
-
# Complicated pattern is also fine.
-
#
-
# get '/complicated_keys' do
-
# required_params :p1, :p2 => [:p3, :p4]
-
# end
-
#
-
# === Classic Application
-
#
-
# In a classic application simply require the helpers, and start using them:
-
#
-
# require "sinatra"
-
# require "sinatra/required_params"
-
#
-
# # The rest of your classic application code goes here...
-
#
-
# === Modular Application
-
#
-
# In a modular application you need to require the helpers, and then tell
-
# the application to use them:
-
#
-
# require "sinatra/base"
-
# require "sinatra/required_params"
-
#
-
# class MyApp < Sinatra::Base
-
# helpers Sinatra::RequiredParams
-
#
-
# # The rest of your modular application code goes here...
-
# end
-
#
-
1
module RequiredParams
-
1
def required_params(*keys)
-
_required_params(params, *keys)
-
end
-
-
1
private
-
-
1
def _required_params(p, *keys)
-
keys.each do |key|
-
if key.is_a?(Hash)
-
_required_params(p, *key.keys)
-
key.each do |k, v|
-
_required_params(p[k.to_s], v)
-
end
-
elsif key.is_a?(Array)
-
_required_params(p, *key)
-
else
-
halt 400 unless p && p.respond_to?(:has_key?) && p.has_key?(key.to_s)
-
end
-
end
-
true
-
end
-
end
-
-
1
helpers RequiredParams
-
end
-
1
require 'sinatra/json'
-
1
require 'sinatra/base'
-
-
1
module Sinatra
-
#
-
# = Sinatra::RespondWith
-
#
-
# These extensions let Sinatra automatically choose what template to render or
-
# action to perform depending on the request's Accept header.
-
#
-
# Example:
-
#
-
# # Without Sinatra::RespondWith
-
# get '/' do
-
# data = { :name => 'example' }
-
# request.accept.each do |type|
-
# case type.to_s
-
# when 'text/html'
-
# halt haml(:index, :locals => data)
-
# when 'text/json'
-
# halt data.to_json
-
# when 'application/atom+xml'
-
# halt nokogiri(:'index.atom', :locals => data)
-
# when 'application/xml', 'text/xml'
-
# halt nokogiri(:'index.xml', :locals => data)
-
# when 'text/plain'
-
# halt 'just an example'
-
# end
-
# end
-
# error 406
-
# end
-
#
-
# # With Sinatra::RespondWith
-
# get '/' do
-
# respond_with :index, :name => 'example' do |f|
-
# f.txt { 'just an example' }
-
# end
-
# end
-
#
-
# Both helper methods +respond_to+ and +respond_with+ let you define custom
-
# handlers like the one above for +text/plain+. +respond_with+ additionally
-
# takes a template name and/or an object to offer the following default
-
# behavior:
-
#
-
# * If a template name is given, search for a template called
-
# +name.format.engine+ (+index.xml.nokogiri+ in the above example).
-
# * If a template name is given, search for a templated called +name.engine+
-
# for engines known to result in the requested format (+index.haml+).
-
# * If a file extension associated with the mime type is known to Sinatra, and
-
# the object responds to +to_extension+, call that method and use the result
-
# (+data.to_json+).
-
#
-
# == Security
-
#
-
# Since methods are triggered based on client input, this can lead to security
-
# issues (but not as severe as those might appear in the first place: keep in
-
# mind that only known file extensions are used). You should limit
-
# the possible formats you serve.
-
#
-
# This is possible with the +provides+ condition:
-
#
-
# get '/', :provides => [:html, :json, :xml, :atom] do
-
# respond_with :index, :name => 'example'
-
# end
-
#
-
# However, since you have to set +provides+ for every route, this extension
-
# adds an app global (class method) `respond_to`, that lets you define content
-
# types for all routes:
-
#
-
# respond_to :html, :json, :xml, :atom
-
# get('/a') { respond_with :index, :name => 'a' }
-
# get('/b') { respond_with :index, :name => 'b' }
-
#
-
# == Custom Types
-
#
-
# Use the +on+ method for defining actions for custom types:
-
#
-
# get '/' do
-
# respond_to do |f|
-
# f.xml { nokogiri :index }
-
# f.on('application/custom') { custom_action }
-
# f.on('text/*') { data.to_s }
-
# f.on('*/*') { "matches everything" }
-
# end
-
# end
-
#
-
# Definition order does not matter.
-
1
module RespondWith
-
1
class Format
-
1
def initialize(app)
-
2
@app, @map, @generic, @default = app, {}, {}, nil
-
end
-
-
1
def on(type, &block)
-
2
@app.settings.mime_types(type).each do |mime|
-
3
case mime
-
when '*/*' then @default = block
-
when /^([^\/]+)\/\*$/ then @generic[$1] = block
-
3
else @map[mime] = block
-
end
-
end
-
end
-
-
1
def finish
-
2
yield self if block_given?
-
2
mime_type = @app.content_type ||
-
@app.request.preferred_type(@map.keys) ||
-
@app.request.preferred_type ||
-
'text/html'
-
2
type = mime_type.split(/\s*;\s*/, 2).first
-
2
handlers = [@map[type], @generic[type[/^[^\/]+/]], @default].compact
-
2
handlers.each do |block|
-
2
if result = block.call(type)
-
2
@app.content_type mime_type
-
2
@app.halt result
-
end
-
end
-
@app.halt 500, "Unknown template engine"
-
end
-
-
1
def method_missing(method, *args, &block)
-
2
return super if args.any? or block.nil? or not @app.mime_type(method)
-
2
on(method, &block)
-
end
-
end
-
-
1
module Helpers
-
1
include Sinatra::JSON
-
-
1
def respond_with(template, object = nil, &block)
-
object, template = template, nil unless Symbol === template
-
format = Format.new(self)
-
format.on "*/*" do |type|
-
exts = settings.ext_map[type]
-
exts << :xml if type.end_with? '+xml'
-
if template
-
args = template_cache.fetch(type, template) { template_for(template, exts) }
-
if args.any?
-
locals = { :object => object }
-
locals.merge! object.to_hash if object.respond_to? :to_hash
-
-
renderer = args.first
-
options = args[1..-1] + [{:locals => locals}]
-
-
halt send(renderer, *options)
-
end
-
end
-
if object
-
exts.each do |ext|
-
halt json(object) if ext == :json
-
next unless object.respond_to? method = "to_#{ext}"
-
halt(*object.send(method))
-
end
-
end
-
false
-
end
-
format.finish(&block)
-
end
-
-
1
def respond_to(&block)
-
2
Format.new(self).finish(&block)
-
end
-
-
1
private
-
-
1
def template_for(name, exts)
-
# in production this is cached, so don't worry too much about runtime
-
possible = []
-
settings.template_engines[:all].each do |engine|
-
exts.each { |ext| possible << [engine, "#{name}.#{ext}"] }
-
end
-
-
exts.each do |ext|
-
settings.template_engines[ext].each { |e| possible << [e, name] }
-
end
-
-
possible.each do |engine, template|
-
klass = Tilt.default_mapping.template_map[engine.to_s] ||
-
Tilt.lazy_map[engine.to_s].fetch(0, [])[0]
-
-
find_template(settings.views, template, klass) do |file|
-
next unless File.exist? file
-
return settings.rendering_method(engine) << template.to_sym
-
end
-
end
-
[] # nil or false would not be cached
-
end
-
end
-
-
1
def remap_extensions
-
16
ext_map.clear
-
10112
Rack::Mime::MIME_TYPES.each { |e,t| ext_map[t] << e[1..-1].to_sym }
-
16
ext_map['text/javascript'] << 'js'
-
16
ext_map['text/xml'] << 'xml'
-
end
-
-
1
def mime_type(*)
-
14
result = super
-
14
remap_extensions
-
14
result
-
end
-
-
1
def respond_to(*formats)
-
25
@respond_to ||= nil
-
-
25
if formats.any?
-
@respond_to ||= []
-
@respond_to.concat formats
-
25
elsif @respond_to.nil? and superclass.respond_to? :respond_to
-
9
superclass.respond_to
-
else
-
16
@respond_to
-
end
-
end
-
-
1
def rendering_method(engine)
-
return [engine] if Sinatra::Templates.method_defined? engine
-
return [:mab] if engine.to_sym == :markaby
-
[:render, :engine]
-
end
-
-
1
private
-
-
1
def compile!(verb, path, block, options = {})
-
16
options[:provides] ||= respond_to if respond_to
-
16
super
-
end
-
-
1
def self.jrubyify(engs)
-
not_supported = [:markdown]
-
engs.keys.each do |key|
-
engs[key].collect! { |eng| (eng == :yajl) ? :json_pure : eng }
-
engs[key].delete_if { |eng| not_supported.include?(eng) }
-
end
-
engs
-
end
-
-
1
def self.engines
-
engines = {
-
2
:css => [:less, :sass, :scss],
-
:xml => [:builder, :nokogiri],
-
:js => [:coffee],
-
:html => [:erb, :erubi, :erubis, :haml, :halmit, :slim, :liquid, :radius,
-
:mab, :markdown, :textile, :rdoc],
-
2
:all => (Sinatra::Templates.instance_methods.map(&:to_sym) +
-
[:mab] - [:find_template, :markaby]),
-
:json => [:yajl],
-
}
-
2
engines.default = []
-
2
(defined? JRUBY_VERSION) ? jrubyify(engines) : engines
-
end
-
-
1
def self.registered(base)
-
8978
base.set :ext_map, Hash.new { |h,k| h[k] = [] }
-
2
base.set :template_engines, engines
-
2
base.remap_extensions
-
2
base.helpers Helpers
-
end
-
end
-
-
1
register RespondWith
-
1
Delegator.delegate :respond_to
-
end
-
1
require 'sinatra/base'
-
-
1
module Sinatra
-
-
# = Sinatra::Streaming
-
#
-
# Sinatra 1.3 introduced the +stream+ helper. This addon improves the
-
# streaming API by making the stream object imitate an IO object, turning
-
# it into a real Deferrable and making the body play nicer with middleware
-
# unaware of streaming.
-
#
-
# == IO-like behavior
-
#
-
# This is useful when passing the stream object to a library expecting an
-
# IO or StringIO object.
-
#
-
# get '/' do
-
# stream do |out|
-
# out.puts "Hello World!", "How are you?"
-
# out.write "Written #{out.pos} bytes so far!\n"
-
# out.putc(65) unless out.closed?
-
# out.flush
-
# end
-
# end
-
#
-
# == Better Middleware Handling
-
#
-
# Blocks passed to #map! or #map will actually be applied when streaming
-
# takes place (as you might have suspected, #map! applies modifications
-
# to the current body, while #map creates a new one):
-
#
-
# class StupidMiddleware
-
# def initialize(app) @app = app end
-
#
-
# def call(env)
-
# status, headers, body = @app.call(env)
-
# body.map! { |e| e.upcase }
-
# [status, headers, body]
-
# end
-
# end
-
#
-
# use StupidMiddleware
-
#
-
# get '/' do
-
# stream do |out|
-
# out.puts "still"
-
# sleep 1
-
# out.puts "streaming"
-
# end
-
# end
-
#
-
# Even works if #each is used to generate an Enumerator:
-
#
-
# def call(env)
-
# status, headers, body = @app.call(env)
-
# body = body.each.map { |s| s.upcase }
-
# [status, headers, body]
-
# end
-
#
-
# Note that both examples violate the Rack specification.
-
#
-
# == Setup
-
#
-
# In a classic application:
-
#
-
# require "sinatra"
-
# require "sinatra/streaming"
-
#
-
# In a modular application:
-
#
-
# require "sinatra/base"
-
# require "sinatra/streaming"
-
#
-
# class MyApp < Sinatra::Base
-
# helpers Sinatra::Streaming
-
# end
-
1
module Streaming
-
1
def stream(*)
-
stream = super
-
stream.extend Stream
-
stream.app = self
-
env['async.close'].callback { stream.close } if env.key? 'async.close'
-
stream
-
end
-
-
1
module Stream
-
-
1
attr_accessor :app, :lineno, :pos, :transformer, :closed
-
1
alias tell pos
-
1
alias closed? closed
-
-
1
def self.extended(obj)
-
obj.closed, obj.lineno, obj.pos = false, 0, 0
-
obj.callback { obj.closed = true }
-
obj.errback { obj.closed = true }
-
end
-
-
1
def <<(data)
-
raise IOError, 'not opened for writing' if closed?
-
-
@transformer ||= nil
-
data = data.to_s
-
data = @transformer[data] if @transformer
-
@pos += data.bytesize
-
super(data)
-
end
-
-
1
def each
-
# that way body.each.map { ... } works
-
return self unless block_given?
-
super
-
end
-
-
1
def map(&block)
-
# dup would not copy the mixin
-
clone.map!(&block)
-
end
-
-
1
def map!(&block)
-
@transformer ||= nil
-
-
if @transformer
-
inner, outer = @transformer, block
-
block = proc { |value| outer[inner[value]] }
-
end
-
@transformer = block
-
self
-
end
-
-
1
def write(data)
-
self << data
-
data.to_s.bytesize
-
end
-
-
1
alias syswrite write
-
1
alias write_nonblock write
-
-
1
def print(*args)
-
args.each { |arg| self << arg }
-
nil
-
end
-
-
1
def printf(format, *args)
-
print(format.to_s % args)
-
end
-
-
1
def putc(c)
-
print c.chr
-
end
-
-
1
def puts(*args)
-
args.each { |arg| self << "#{arg}\n" }
-
nil
-
end
-
-
1
def close_read
-
raise IOError, "closing non-duplex IO for reading"
-
end
-
-
1
def closed_read?
-
true
-
end
-
-
1
def closed_write?
-
closed?
-
end
-
-
1
def external_encoding
-
Encoding.find settings.default_encoding
-
rescue NameError
-
settings.default_encoding
-
end
-
-
1
def closed?
-
@closed
-
end
-
-
1
def settings
-
app.settings
-
end
-
-
1
def rewind
-
@pos = @lineno = 0
-
end
-
-
1
def not_open_for_reading(*)
-
raise IOError, "not opened for reading"
-
end
-
-
1
alias bytes not_open_for_reading
-
1
alias eof? not_open_for_reading
-
1
alias eof not_open_for_reading
-
1
alias getbyte not_open_for_reading
-
1
alias getc not_open_for_reading
-
1
alias gets not_open_for_reading
-
1
alias read not_open_for_reading
-
1
alias read_nonblock not_open_for_reading
-
1
alias readbyte not_open_for_reading
-
1
alias readchar not_open_for_reading
-
1
alias readline not_open_for_reading
-
1
alias readlines not_open_for_reading
-
1
alias readpartial not_open_for_reading
-
1
alias sysread not_open_for_reading
-
1
alias ungetbyte not_open_for_reading
-
1
alias ungetc not_open_for_reading
-
1
private :not_open_for_reading
-
-
1
def enum_not_open_for_reading(*)
-
not_open_for_reading if block_given?
-
enum_for(:not_open_for_reading)
-
end
-
-
1
alias chars enum_not_open_for_reading
-
1
alias each_line enum_not_open_for_reading
-
1
alias each_byte enum_not_open_for_reading
-
1
alias each_char enum_not_open_for_reading
-
1
alias lines enum_not_open_for_reading
-
1
undef enum_not_open_for_reading
-
-
1
def dummy(*) end
-
1
alias flush dummy
-
1
alias fsync dummy
-
1
alias internal_encoding dummy
-
1
alias pid dummy
-
1
undef dummy
-
-
1
def seek(*)
-
0
-
end
-
-
1
alias sysseek seek
-
-
1
def sync
-
true
-
end
-
-
1
def tty?
-
false
-
end
-
-
1
alias isatty tty?
-
end
-
end
-
-
1
helpers Streaming
-
end
-
# encoding: utf-8
-
# frozen_string_literal: true
-
-
1
require 'sprockets/version'
-
1
require 'sprockets/cache'
-
1
require 'sprockets/environment'
-
1
require 'sprockets/errors'
-
1
require 'sprockets/manifest'
-
-
1
module Sprockets
-
1
require 'sprockets/processor_utils'
-
1
extend ProcessorUtils
-
-
# Extend Sprockets module to provide global registry
-
1
require 'sprockets/configuration'
-
1
require 'sprockets/context'
-
1
require 'digest/sha2'
-
1
extend Configuration
-
-
1
self.config = {
-
2
bundle_processors: Hash.new { |h, k| [].freeze }.freeze,
-
7
bundle_reducers: Hash.new { |h, k| {}.freeze }.freeze,
-
2
compressors: Hash.new { |h, k| {}.freeze }.freeze,
-
dependencies: Set.new.freeze,
-
dependency_resolvers: {}.freeze,
-
digest_class: Digest::SHA256,
-
mime_exts: {}.freeze,
-
mime_types: {}.freeze,
-
paths: [].freeze,
-
pipelines: {}.freeze,
-
pipeline_exts: {}.freeze,
-
949
postprocessors: Hash.new { |h, k| [].freeze }.freeze,
-
443
preprocessors: Hash.new { |h, k| [].freeze }.freeze,
-
registered_transformers: [].freeze,
-
root: __dir__.dup.freeze,
-
transformers: Hash.new { |h, k| {}.freeze }.freeze,
-
1
exporters: Hash.new { |h, k| Set.new.freeze }.freeze,
-
version: "",
-
gzip_enabled: true,
-
export_concurrent: true
-
}.freeze
-
-
1
@context_class = Context
-
-
1
require 'logger'
-
1
@logger = Logger.new($stderr)
-
1
@logger.level = Logger::FATAL
-
-
# Common asset text types
-
1
register_mime_type 'application/javascript', extensions: ['.js'], charset: :unicode
-
1
register_mime_type 'application/json', extensions: ['.json'], charset: :unicode
-
1
register_mime_type 'application/ruby', extensions: ['.rb']
-
1
register_mime_type 'application/xml', extensions: ['.xml']
-
1
register_mime_type 'text/css', extensions: ['.css'], charset: :css
-
1
register_mime_type 'text/html', extensions: ['.html', '.htm'], charset: :html
-
1
register_mime_type 'text/plain', extensions: ['.txt', '.text']
-
1
register_mime_type 'text/yaml', extensions: ['.yml', '.yaml'], charset: :unicode
-
-
# Common image types
-
1
register_mime_type 'image/x-icon', extensions: ['.ico']
-
1
register_mime_type 'image/bmp', extensions: ['.bmp']
-
1
register_mime_type 'image/gif', extensions: ['.gif']
-
1
register_mime_type 'image/webp', extensions: ['.webp']
-
1
register_mime_type 'image/png', extensions: ['.png']
-
1
register_mime_type 'image/jpeg', extensions: ['.jpg', '.jpeg']
-
1
register_mime_type 'image/tiff', extensions: ['.tiff', '.tif']
-
1
register_mime_type 'image/svg+xml', extensions: ['.svg']
-
-
# Common audio/video types
-
1
register_mime_type 'video/webm', extensions: ['.webm']
-
1
register_mime_type 'audio/basic', extensions: ['.snd', '.au']
-
1
register_mime_type 'audio/aiff', extensions: ['.aiff']
-
1
register_mime_type 'audio/mpeg', extensions: ['.mp3', '.mp2', '.m2a', '.m3a']
-
1
register_mime_type 'application/ogg', extensions: ['.ogx']
-
1
register_mime_type 'audio/ogg', extensions: ['.ogg', '.oga']
-
1
register_mime_type 'audio/midi', extensions: ['.midi', '.mid']
-
1
register_mime_type 'video/avi', extensions: ['.avi']
-
1
register_mime_type 'audio/wave', extensions: ['.wav', '.wave']
-
1
register_mime_type 'video/mp4', extensions: ['.mp4', '.m4v']
-
1
register_mime_type 'audio/aac', extensions: ['.aac']
-
1
register_mime_type 'audio/mp4', extensions: ['.m4a']
-
1
register_mime_type 'audio/flac', extensions: ['.flac']
-
-
# Common font types
-
1
register_mime_type 'application/vnd.ms-fontobject', extensions: ['.eot']
-
1
register_mime_type 'application/x-font-opentype', extensions: ['.otf']
-
1
register_mime_type 'application/x-font-ttf', extensions: ['.ttf']
-
1
register_mime_type 'application/font-woff', extensions: ['.woff']
-
1
register_mime_type 'application/font-woff2', extensions: ['.woff2']
-
-
1
require 'sprockets/source_map_processor'
-
1
register_mime_type 'application/js-sourcemap+json', extensions: ['.js.map']
-
1
register_mime_type 'application/css-sourcemap+json', extensions: ['.css.map']
-
1
register_transformer 'application/javascript', 'application/js-sourcemap+json', SourceMapProcessor
-
1
register_transformer 'text/css', 'application/css-sourcemap+json', SourceMapProcessor
-
-
1
register_pipeline :source do |env|
-
[]
-
end
-
-
1
register_pipeline :self do |env, type, file_type|
-
7
env.self_processors_for(type, file_type)
-
end
-
-
1
register_pipeline :default do |env, type, file_type|
-
4
env.default_processors_for(type, file_type)
-
end
-
-
1
require 'sprockets/add_source_map_comment_to_asset_processor'
-
1
register_pipeline :debug do
-
[AddSourceMapCommentToAssetProcessor]
-
end
-
-
1
require 'sprockets/directive_processor'
-
1
register_preprocessor 'text/css', DirectiveProcessor.new(comments: ["//", ["/*", "*/"]])
-
1
register_preprocessor 'application/javascript', DirectiveProcessor.new(comments: ["//", ["/*", "*/"]])
-
-
1
require 'sprockets/bundle'
-
1
register_bundle_processor 'application/javascript', Bundle
-
1
register_bundle_processor 'text/css', Bundle
-
-
2
register_bundle_metadata_reducer '*/*', :data, proc { +"" }, :concat
-
2
register_bundle_metadata_reducer 'application/javascript', :data, proc { +"" }, Utils.method(:concat_javascript_sources)
-
1
register_bundle_metadata_reducer '*/*', :links, :+
-
3
register_bundle_metadata_reducer '*/*', :sources, proc { [] }, :+
-
-
1
require 'sprockets/closure_compressor'
-
1
require 'sprockets/sass_compressor'
-
1
require 'sprockets/sassc_compressor'
-
1
require 'sprockets/jsminc_compressor'
-
1
require 'sprockets/uglifier_compressor'
-
1
require 'sprockets/yui_compressor'
-
1
register_compressor 'text/css', :sass, SassCompressor
-
1
register_compressor 'text/css', :sassc, SasscCompressor
-
1
register_compressor 'text/css', :scss, SassCompressor
-
1
register_compressor 'text/css', :scssc, SasscCompressor
-
1
register_compressor 'text/css', :yui, YUICompressor
-
1
register_compressor 'application/javascript', :closure, ClosureCompressor
-
1
register_compressor 'application/javascript', :jsmin, JSMincCompressor
-
1
register_compressor 'application/javascript', :jsminc, JSMincCompressor
-
1
register_compressor 'application/javascript', :uglifier, UglifierCompressor
-
1
register_compressor 'application/javascript', :uglify, UglifierCompressor
-
1
register_compressor 'application/javascript', :yui, YUICompressor
-
-
# Babel, TheFutureâ„¢ is now
-
1
require 'sprockets/babel_processor'
-
1
register_mime_type 'application/ecmascript-6', extensions: ['.es6'], charset: :unicode
-
1
register_transformer 'application/ecmascript-6', 'application/javascript', BabelProcessor
-
1
register_preprocessor 'application/ecmascript-6', DirectiveProcessor.new(comments: ["//", ["/*", "*/"]])
-
-
# Mmm, CoffeeScript
-
1
require 'sprockets/coffee_script_processor'
-
1
register_mime_type 'text/coffeescript', extensions: ['.coffee', '.js.coffee']
-
1
register_transformer 'text/coffeescript', 'application/javascript', CoffeeScriptProcessor
-
1
register_preprocessor 'text/coffeescript', DirectiveProcessor.new(comments: ["#", ["###", "###"]])
-
-
# JST processors
-
1
require 'sprockets/eco_processor'
-
1
require 'sprockets/ejs_processor'
-
1
require 'sprockets/jst_processor'
-
1
register_mime_type 'text/eco', extensions: ['.eco', '.jst.eco']
-
1
register_mime_type 'text/ejs', extensions: ['.ejs', '.jst.ejs']
-
1
register_transformer 'text/eco', 'application/javascript+function', EcoProcessor
-
1
register_transformer 'text/ejs', 'application/javascript+function', EjsProcessor
-
1
register_transformer 'application/javascript+function', 'application/javascript', JstProcessor
-
-
# CSS processors
-
1
require 'sprockets/sassc_processor'
-
1
register_mime_type 'text/sass', extensions: ['.sass', '.css.sass']
-
1
register_mime_type 'text/scss', extensions: ['.scss', '.css.scss']
-
1
register_transformer 'text/sass', 'text/css', SasscProcessor
-
1
register_transformer 'text/scss', 'text/css', ScsscProcessor
-
1
register_preprocessor 'text/sass', DirectiveProcessor.new(comments: ["//", ["/*", "*/"]])
-
1
register_preprocessor 'text/scss', DirectiveProcessor.new(comments: ["//", ["/*", "*/"]])
-
1
register_bundle_metadata_reducer 'text/css', :sass_dependencies, Set.new, :+
-
-
# ERB
-
1
require 'sprockets/erb_processor'
-
1
register_transformer_suffix(%w(
-
application/ecmascript-6
-
application/javascript
-
application/json
-
application/xml
-
text/coffeescript
-
text/css
-
text/html
-
text/plain
-
text/sass
-
text/scss
-
text/yaml
-
text/eco
-
), 'application/\2+ruby', '.erb', ERBProcessor)
-
-
1
register_mime_type 'application/html+ruby', extensions: ['.html.erb', '.erb', '.rhtml'], charset: :html
-
1
register_mime_type 'application/xml+ruby', extensions: ['.xml.erb', '.rxml']
-
-
1
require 'sprockets/exporters/file_exporter'
-
1
require 'sprockets/exporters/zlib_exporter'
-
1
require 'sprockets/exporters/zopfli_exporter'
-
1
register_exporter '*/*', Exporters::FileExporter
-
1
register_exporter '*/*', Exporters::ZlibExporter
-
-
1
register_dependency_resolver 'environment-version' do |env|
-
2
env.version
-
end
-
1
register_dependency_resolver 'environment-paths' do |env|
-
6
env.paths.map {|path| env.compress_from_root(path) }
-
end
-
1
register_dependency_resolver 'file-digest' do |env, str|
-
16
env.file_digest(env.parse_file_digest_uri(str))
-
end
-
1
register_dependency_resolver 'processors' do |env, str|
-
5
env.resolve_processors_cache_key_uri(str)
-
end
-
1
register_dependency_resolver 'env' do |env, str|
-
_, var = str.split(':', 2)
-
ENV[var]
-
end
-
-
1
depend_on 'environment-version'
-
1
depend_on 'environment-paths'
-
-
1
require 'sprockets/preprocessors/default_source_map'
-
1
register_preprocessor 'text/css', Preprocessors::DefaultSourceMap.new
-
1
register_preprocessor 'application/javascript', Preprocessors::DefaultSourceMap.new
-
-
2
register_bundle_metadata_reducer 'text/css', :map, proc { |input| { "version" => 3, "file" => PathUtils.split_subpath(input[:load_path], input[:filename]), "sections" => [] } }, SourceMapUtils.method(:concat_source_maps)
-
2
register_bundle_metadata_reducer 'application/javascript', :map, proc { |input| { "version" => 3, "file" => PathUtils.split_subpath(input[:load_path], input[:filename]), "sections" => [] } }, SourceMapUtils.method(:concat_source_maps)
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/uri_utils'
-
1
require 'sprockets/path_utils'
-
-
1
module Sprockets
-
# This is a processor designed to add a source map "comment"
-
# to the bottom of a css or JS file that is serving a source
-
# map. An example of a comment might look like this
-
#
-
# //# application.js-80af0efcc960fc2ac93eda2f7b12e3db40ab360bf6ea269ceed3bea3678326f9.map
-
#
-
# As an asset is built it gets source map information added
-
# to the `asset.to_hash[:metadata][:map]` key. This contains all the
-
# information that is needed to build a source map file.
-
#
-
# To add this comment we must have an asset we can link to.
-
# To do this we ensure that the original aset is loaded, then
-
# we use a use a special mime type. For example `application/js-sourcemap+json`
-
# for a JS source map.
-
#
-
# This will trigger a new asset to be loaded and generated by the
-
# `SourceMapProcessor` processor.
-
#
-
# Finally once we have that file, we can generate a link to it
-
# with it's full fingerprint. This is done and then
-
# added to the original asset as a comment at the bottom.
-
#
-
1
class AddSourceMapCommentToAssetProcessor
-
1
def self.call(input)
-
-
case input[:content_type]
-
when "application/javascript"
-
comment = "\n//# sourceMappingURL=%s"
-
map_type = "application/js-sourcemap+json"
-
when "text/css"
-
comment = "\n/*# sourceMappingURL=%s */"
-
map_type = "application/css-sourcemap+json"
-
else
-
fail input[:content_type]
-
end
-
-
env = input[:environment]
-
-
uri, _ = env.resolve!(input[:filename], accept: input[:content_type])
-
asset = env.load(uri)
-
-
uri, _ = env.resolve!(input[:filename], accept: map_type)
-
map = env.load(uri)
-
-
uri, params = URIUtils.parse_asset_uri(input[:uri])
-
uri = env.expand_from_root(params[:index_alias]) if params[:index_alias]
-
path = PathUtils.relative_path_from(PathUtils.split_subpath(input[:load_path], uri), map.digest_path)
-
-
asset.metadata.merge(
-
data: asset.source + (comment % path) + "\n",
-
links: asset.links + [asset.uri, map.uri]
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Sprockets
-
1
module Autoload
-
1
autoload :Babel, 'sprockets/autoload/babel'
-
1
autoload :Closure, 'sprockets/autoload/closure'
-
1
autoload :CoffeeScript, 'sprockets/autoload/coffee_script'
-
1
autoload :Eco, 'sprockets/autoload/eco'
-
1
autoload :EJS, 'sprockets/autoload/ejs'
-
1
autoload :JSMinC, 'sprockets/autoload/jsminc'
-
1
autoload :Sass, 'sprockets/autoload/sass'
-
1
autoload :SassC, 'sprockets/autoload/sassc'
-
1
autoload :Uglifier, 'sprockets/autoload/uglifier'
-
1
autoload :YUI, 'sprockets/autoload/yui'
-
1
autoload :Zopfli, 'sprockets/autoload/zopfli'
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sassc'
-
-
1
module Sprockets
-
1
module Autoload
-
1
SassC = ::SassC
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/autoload'
-
1
require 'sprockets/path_utils'
-
1
require 'sprockets/source_map_utils'
-
1
require 'json'
-
-
1
module Sprockets
-
1
class BabelProcessor
-
1
VERSION = '1'
-
-
1
def self.instance
-
@instance ||= new
-
end
-
-
1
def self.call(input)
-
instance.call(input)
-
end
-
-
1
def self.cache_key
-
instance.cache_key
-
end
-
-
1
attr_reader :cache_key
-
-
1
def initialize(options = {})
-
@options = options.merge({
-
'blacklist' => (options['blacklist'] || []) + ['useStrict'],
-
'sourceMap' => true
-
}).freeze
-
-
@cache_key = [
-
self.class.name,
-
Autoload::Babel::Transpiler::VERSION,
-
Autoload::Babel::Source::VERSION,
-
VERSION,
-
@options
-
].freeze
-
end
-
-
1
def call(input)
-
data = input[:data]
-
-
result = input[:cache].fetch(@cache_key + [input[:filename]] + [data]) do
-
opts = {
-
'moduleRoot' => nil,
-
'filename' => input[:filename],
-
'filenameRelative' => PathUtils.split_subpath(input[:load_path], input[:filename]),
-
'sourceFileName' => File.basename(input[:filename]),
-
'sourceMapTarget' => input[:filename]
-
}.merge(@options)
-
-
if opts['moduleIds'] && opts['moduleRoot']
-
opts['moduleId'] ||= File.join(opts['moduleRoot'], input[:name])
-
elsif opts['moduleIds']
-
opts['moduleId'] ||= input[:name]
-
end
-
Autoload::Babel::Transpiler.transform(data, opts)
-
end
-
-
map = SourceMapUtils.format_source_map(result["map"], input)
-
map = SourceMapUtils.combine_source_maps(input[:metadata][:map], map)
-
-
{ data: result['code'], map: map }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/asset'
-
1
require 'sprockets/bower'
-
1
require 'sprockets/cache'
-
1
require 'sprockets/configuration'
-
1
require 'sprockets/digest_utils'
-
1
require 'sprockets/errors'
-
1
require 'sprockets/loader'
-
1
require 'sprockets/npm'
-
1
require 'sprockets/path_dependency_utils'
-
1
require 'sprockets/path_digest_utils'
-
1
require 'sprockets/path_utils'
-
1
require 'sprockets/resolve'
-
1
require 'sprockets/server'
-
1
require 'sprockets/source_map_utils'
-
1
require 'sprockets/uri_tar'
-
-
1
module Sprockets
-
-
1
class DoubleLinkError < Sprockets::Error
-
1
def initialize(parent_filename:, logical_path:, last_filename:, filename:)
-
super <<~MSG
-
Multiple files with the same output path cannot be linked (#{logical_path.inspect})
-
In #{parent_filename.inspect} these files were linked:
-
- #{last_filename}
-
- #{filename}
-
MSG
-
end
-
end
-
-
# `Base` class for `Environment` and `CachedEnvironment`.
-
1
class Base
-
1
include PathUtils, PathDependencyUtils, PathDigestUtils, DigestUtils, SourceMapUtils
-
1
include Configuration
-
1
include Server
-
1
include Resolve, Loader
-
1
include Bower
-
1
include Npm
-
-
# Get persistent cache store
-
1
attr_reader :cache
-
-
# Set persistent cache store
-
#
-
# The cache store must implement a pair of getters and
-
# setters. Either `get(key)`/`set(key, value)`,
-
# `[key]`/`[key]=value`, `read(key)`/`write(key, value)`.
-
1
def cache=(cache)
-
1
@cache = Cache.new(cache, logger)
-
end
-
-
# Return an `CachedEnvironment`. Must be implemented by the subclass.
-
1
def cached
-
raise NotImplementedError
-
end
-
1
alias_method :index, :cached
-
-
# Internal: Compute digest for path.
-
#
-
# path - String filename or directory path.
-
#
-
# Returns a String digest or nil.
-
1
def file_digest(path)
-
22
if stat = self.stat(path)
-
# Caveat: Digests are cached by the path's current mtime. Its possible
-
# for a files contents to have changed and its mtime to have been
-
# negligently reset thus appearing as if the file hasn't changed on
-
# disk. Also, the mtime is only read to the nearest second. It's
-
# also possible the file was updated more than once in a given second.
-
20
key = UnloadedAsset.new(path, self).file_digest_key(stat.mtime.to_i)
-
20
cache.fetch(key) do
-
14
self.stat_digest(path, stat)
-
end
-
end
-
end
-
-
# Find asset by logical path or expanded path.
-
1
def find_asset(*args, **options)
-
2
uri, _ = resolve(*args, **options)
-
2
if uri
-
2
load(uri)
-
end
-
end
-
-
1
def find_all_linked_assets(*args)
-
return to_enum(__method__, *args) unless block_given?
-
-
parent_asset = asset = find_asset(*args)
-
return unless asset
-
-
yield asset
-
stack = asset.links.to_a
-
linked_paths = {}
-
-
while uri = stack.shift
-
yield asset = load(uri)
-
-
last_filename = linked_paths[asset.logical_path]
-
if last_filename && last_filename != asset.filename
-
raise DoubleLinkError.new(
-
parent_filename: parent_asset.filename,
-
last_filename: last_filename,
-
logical_path: asset.logical_path,
-
filename: asset.filename
-
)
-
end
-
linked_paths[asset.logical_path] = asset.filename
-
stack = asset.links.to_a + stack
-
end
-
-
nil
-
end
-
-
# Preferred `find_asset` shorthand.
-
#
-
# environment['application.js']
-
#
-
1
def [](*args, **options)
-
find_asset(*args, **options)
-
end
-
-
# Find asset by logical path or expanded path.
-
#
-
# If the asset is not found an error will be raised.
-
1
def find_asset!(*args)
-
uri, _ = resolve!(*args)
-
if uri
-
load(uri)
-
end
-
end
-
-
# Pretty inspect
-
1
def inspect
-
"#<#{self.class}:0x#{object_id.to_s(16)} " +
-
"root=#{root.to_s.inspect}, " +
-
"paths=#{paths.inspect}>"
-
end
-
-
1
def compress_from_root(uri)
-
92
URITar.new(uri, self).compress
-
end
-
-
1
def expand_from_root(uri)
-
URITar.new(uri, self).expand
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'json'
-
-
1
module Sprockets
-
1
module Bower
-
# Internal: All supported bower.json files.
-
#
-
# https://github.com/bower/json/blob/0.4.0/lib/json.js#L7
-
1
POSSIBLE_BOWER_JSONS = ['bower.json', 'component.json', '.bower.json']
-
-
# Internal: Override resolve_alternates to install bower.json behavior.
-
#
-
# load_path - String environment path
-
# logical_path - String path relative to base
-
#
-
# Returns candiate filenames.
-
1
def resolve_alternates(load_path, logical_path)
-
4
candidates, deps = super
-
-
# bower.json can only be nested one level deep
-
4
if !logical_path.index('/'.freeze)
-
4
dirname = File.join(load_path, logical_path)
-
-
4
if directory?(dirname)
-
filenames = POSSIBLE_BOWER_JSONS.map { |basename| File.join(dirname, basename) }
-
filename = filenames.detect { |fn| self.file?(fn) }
-
-
if filename
-
deps << build_file_digest_uri(filename)
-
read_bower_main(dirname, filename) do |path|
-
if file?(path)
-
candidates << path
-
end
-
end
-
end
-
end
-
end
-
-
4
return candidates, deps
-
end
-
-
# Internal: Read bower.json's main directive.
-
#
-
# dirname - String path to component directory.
-
# filename - String path to bower.json.
-
#
-
# Returns nothing.
-
1
def read_bower_main(dirname, filename)
-
bower = JSON.parse(File.read(filename), create_additions: false)
-
-
case bower['main']
-
when String
-
yield File.expand_path(bower['main'], dirname)
-
when Array
-
bower['main'].each do |name|
-
yield File.expand_path(name, dirname)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'set'
-
1
require 'sprockets/utils'
-
1
require 'sprockets/uri_utils'
-
-
1
module Sprockets
-
# Internal: Bundle processor takes a single file asset and prepends all the
-
# `:required` URIs to the contents.
-
#
-
# Uses pipeline metadata:
-
#
-
# :required - Ordered Set of asset URIs to prepend
-
# :stubbed - Set of asset URIs to substract from the required set.
-
#
-
# Also see DirectiveProcessor.
-
1
class Bundle
-
1
def self.call(input)
-
2
env = input[:environment]
-
2
type = input[:content_type]
-
2
input[:links] ||= Set.new
-
2
dependencies = Set.new(input[:metadata][:dependencies])
-
-
2
processed_uri, deps = env.resolve(input[:filename], accept: type, pipeline: :self)
-
2
dependencies.merge(deps)
-
-
# DirectiveProcessor (and any other transformers called here with pipeline=self)
-
2
primary_asset = env.load(processed_uri)
-
2
to_load = primary_asset.metadata.delete(:to_load) || Set.new
-
2
to_link = primary_asset.metadata.delete(:to_link) || Set.new
-
-
2
to_load.each do |uri|
-
loaded_asset = env.load(uri)
-
dependencies.merge(loaded_asset.metadata[:dependencies])
-
if to_link.include?(uri)
-
primary_metadata = primary_asset.metadata
-
input[:links] << loaded_asset.uri
-
primary_metadata[:links] << loaded_asset.uri
-
end
-
end
-
-
6
find_required = proc { |uri| env.load(uri).metadata[:required] }
-
2
required = Utils.dfs(processed_uri, &find_required)
-
2
stubbed = Utils.dfs(env.load(processed_uri).metadata[:stubbed], &find_required)
-
2
required.subtract(stubbed)
-
2
dedup(required)
-
6
assets = required.map { |uri| env.load(uri) }
-
-
2
(required + stubbed).each do |uri|
-
4
dependencies.merge(env.load(uri).metadata[:dependencies])
-
end
-
-
2
reducers = Hash[env.match_mime_type_keys(env.config[:bundle_reducers], type).flat_map(&:to_a)]
-
2
process_bundle_reducers(input, assets, reducers).merge(dependencies: dependencies, included: assets.map(&:uri))
-
end
-
-
# Internal: Removes uri from required if it's already included as an alias.
-
#
-
# required - Set of required uris
-
#
-
# Returns deduped set of uris
-
1
def self.dedup(required)
-
2
dupes = required.reduce([]) do |r, uri|
-
4
path, params = URIUtils.parse_asset_uri(uri)
-
4
if (params.delete(:index_alias))
-
r << URIUtils.build_asset_uri(path, params)
-
end
-
4
r
-
end
-
2
required.subtract(dupes)
-
end
-
-
# Internal: Run bundle reducers on set of Assets producing a reduced
-
# metadata Hash.
-
#
-
# filename - String bundle filename
-
# assets - Array of Assets
-
# reducers - Array of [initial, reducer_proc] pairs
-
#
-
# Returns reduced asset metadata Hash.
-
1
def self.process_bundle_reducers(input, assets, reducers)
-
2
initial = {}
-
2
reducers.each do |k, (v, _)|
-
9
if v.respond_to?(:call)
-
6
initial[k] = v.call(input)
-
3
elsif !v.nil?
-
1
initial[k] = v
-
end
-
end
-
-
2
assets.reduce(initial) do |h, asset|
-
4
reducers.each do |k, (_, block)|
-
18
value = k == :data ? asset.source : asset.metadata[k]
-
18
if h.key?(k)
-
16
if !value.nil?
-
10
h[k] = block.call(h[k], value)
-
end
-
else
-
2
h[k] = value
-
end
-
end
-
4
h
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'logger'
-
1
require 'sprockets/digest_utils'
-
-
1
module Sprockets
-
# Public: Wrapper interface to backend cache stores. Ensures a consistent API
-
# even when the backend uses get/set or read/write.
-
#
-
# Public cache interface
-
#
-
# Always assign the backend store instance to Environment#cache=.
-
#
-
# environment.cache = Sprockets::Cache::MemoryStore.new(1000)
-
#
-
# Environment#cache will always return a wrapped Cache interface. See the
-
# methods marked public on this class.
-
#
-
#
-
# Backend cache interface
-
#
-
# The Backend cache store must implement two methods.
-
#
-
# get(key)
-
#
-
# key - An opaque String with a length less than 250 characters.
-
#
-
# Returns an JSON serializable object.
-
#
-
# set(key, value)
-
#
-
# Will only be called once per key. Setting a key "foo" with value "bar",
-
# then later key "foo" with value "baz" is an undefined behavior.
-
#
-
# key - An opaque String with a length less than 250 characters.
-
# value - A JSON serializable object.
-
#
-
# Returns argument value.
-
#
-
# clear(options)
-
#
-
# Clear the entire cache. Be careful with this method since it could
-
# affect other processes if shared cache is being used.
-
#
-
# The options hash is passed to the underlying cache implementation.
-
1
class Cache
-
# Builtin cache stores.
-
1
autoload :FileStore, 'sprockets/cache/file_store'
-
1
autoload :MemoryStore, 'sprockets/cache/memory_store'
-
1
autoload :NullStore, 'sprockets/cache/null_store'
-
-
# Internal: Cache key version for this class. Rarely should have to change
-
# unless the cache format radically changes. Will be bump on major version
-
# releases though.
-
1
VERSION = '4.0.0'
-
-
1
def self.default_logger
-
logger = Logger.new($stderr)
-
logger.level = Logger::FATAL
-
logger
-
end
-
-
# Internal: Wrap a backend cache store.
-
#
-
# Always assign a backend cache store instance to Environment#cache= and
-
# use Environment#cache to retreive a wrapped interface.
-
#
-
# cache - A compatible backend cache store instance.
-
1
def initialize(cache = nil, logger = self.class.default_logger)
-
1
@cache_wrapper = get_cache_wrapper(cache)
-
1
@fetch_cache = Cache::MemoryStore.new(1024)
-
1
@logger = logger
-
end
-
-
# Public: Prefer API to retrieve and set values in the cache store.
-
#
-
# key - JSON serializable key
-
# block -
-
# Must return a consistent JSON serializable object for the given key.
-
#
-
# Examples
-
#
-
# cache.fetch("foo") { "bar" }
-
#
-
# Returns a JSON serializable object.
-
1
def fetch(key)
-
20
start = Time.now.to_f
-
20
expanded_key = expand_key(key)
-
20
value = @fetch_cache.get(expanded_key)
-
20
if value.nil?
-
14
value = @cache_wrapper.get(expanded_key)
-
14
if value.nil?
-
14
value = yield
-
14
@cache_wrapper.set(expanded_key, value)
-
14
@logger.debug do
-
ms = "(#{((Time.now.to_f - start) * 1000).to_i}ms)"
-
"Sprockets Cache miss #{peek_key(key)} #{ms}"
-
end
-
end
-
14
@fetch_cache.set(expanded_key, value)
-
end
-
20
value
-
end
-
-
# Public: Low level API to retrieve item directly from the backend cache
-
# store.
-
#
-
# This API may be used publicly, but may have undefined behavior
-
# depending on the backend store being used. Prefer the
-
# Cache#fetch API over using this.
-
#
-
# key - JSON serializable key
-
# local - Check local cache first (default: false)
-
#
-
# Returns a JSON serializable object or nil if there was a cache miss.
-
1
def get(key, local = false)
-
6
expanded_key = expand_key(key)
-
-
6
if local && value = @fetch_cache.get(expanded_key)
-
return value
-
end
-
-
6
value = @cache_wrapper.get(expanded_key)
-
6
@fetch_cache.set(expanded_key, value) if local
-
-
6
value
-
end
-
-
# Public: Low level API to set item directly to the backend cache store.
-
#
-
# This API may be used publicly, but may have undefined behavior
-
# depending on the backend store being used. Prefer the
-
# Cache#fetch API over using this.
-
#
-
# key - JSON serializable key
-
# value - A consistent JSON serializable object for the given key. Setting
-
# a different value for the given key has undefined behavior.
-
# local - Set on local cache (default: false)
-
#
-
# Returns the value argument.
-
1
def set(key, value, local = false)
-
18
expanded_key = expand_key(key)
-
18
@fetch_cache.set(expanded_key, value) if local
-
18
@cache_wrapper.set(expanded_key, value)
-
end
-
-
# Public: Pretty inspect
-
#
-
# Returns String.
-
1
def inspect
-
"#<#{self.class} local=#{@fetch_cache.inspect} store=#{@cache_wrapper.cache.inspect}>"
-
end
-
-
# Public: Clear cache
-
#
-
# Returns truthy on success, potentially raises exception on failure
-
1
def clear(options=nil)
-
@cache_wrapper.clear
-
@fetch_cache.clear
-
end
-
-
1
private
-
# Internal: Expand object cache key into a short String key.
-
#
-
# The String should be under 250 characters so its compatible with
-
# Memcache.
-
#
-
# key - JSON serializable key
-
#
-
# Returns a String with a length less than 250 characters.
-
1
def expand_key(key)
-
44
digest_key = DigestUtils.pack_urlsafe_base64digest(DigestUtils.digest(key))
-
44
namespace = digest_key[0, 2]
-
44
"sprockets/v#{VERSION}/#{namespace}/#{digest_key}"
-
end
-
-
1
PEEK_SIZE = 100
-
-
# Internal: Show first 100 characters of cache key for logging purposes.
-
#
-
# Returns a String with a length less than 100 characters.
-
1
def peek_key(key)
-
case key
-
when Integer
-
key.to_s
-
when String
-
key[0, PEEK_SIZE].inspect
-
when Array
-
str = []
-
key.each { |k| str << peek_key(k) }
-
str.join(':')[0, PEEK_SIZE]
-
else
-
peek_key(DigestUtils.pack_urlsafe_base64digest(DigestUtils.digest(key)))
-
end
-
end
-
-
1
def get_cache_wrapper(cache)
-
1
if cache.is_a?(Cache)
-
cache
-
-
# `Cache#get(key)` for Memcache
-
1
elsif cache.respond_to?(:get)
-
1
GetWrapper.new(cache)
-
-
# `Cache#[key]` so `Hash` can be used
-
elsif cache.respond_to?(:[])
-
HashWrapper.new(cache)
-
-
# `Cache#read(key)` for `ActiveSupport::Cache` support
-
elsif cache.respond_to?(:read)
-
ReadWriteWrapper.new(cache)
-
-
else
-
cache = Sprockets::Cache::NullStore.new
-
GetWrapper.new(cache)
-
end
-
end
-
-
1
class Wrapper < Struct.new(:cache)
-
end
-
-
1
class GetWrapper < Wrapper
-
1
def get(key)
-
20
cache.get(key)
-
end
-
-
1
def set(key, value)
-
32
cache.set(key, value)
-
end
-
-
1
def clear(options=nil)
-
# dalli has a #flush method so try it
-
if cache.respond_to?(:flush)
-
cache.flush(options)
-
else
-
cache.clear(options)
-
end
-
true
-
end
-
end
-
-
1
class HashWrapper < Wrapper
-
1
def get(key)
-
cache[key]
-
end
-
-
1
def set(key, value)
-
cache[key] = value
-
end
-
-
1
def clear(options=nil)
-
cache.clear
-
true
-
end
-
end
-
-
1
class ReadWriteWrapper < Wrapper
-
1
def get(key)
-
cache.read(key)
-
end
-
-
1
def set(key, value)
-
cache.write(key, value)
-
end
-
-
1
def clear(options=nil)
-
cache.clear(options)
-
true
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Sprockets
-
1
class Cache
-
# Public: Basic in memory LRU cache.
-
#
-
# Assign the instance to the Environment#cache.
-
#
-
# environment.cache = Sprockets::Cache::MemoryStore.new(1000)
-
#
-
# See Also
-
#
-
# ActiveSupport::Cache::MemoryStore
-
#
-
1
class MemoryStore
-
# Internal: Default key limit for store.
-
1
DEFAULT_MAX_SIZE = 1000
-
-
# Public: Initialize the cache store.
-
#
-
# max_size - A Integer of the maximum number of keys the store will hold.
-
# (default: 1000).
-
1
def initialize(max_size = DEFAULT_MAX_SIZE)
-
2
@max_size = max_size
-
2
@cache = {}
-
end
-
-
# Public: Retrieve value from cache.
-
#
-
# This API should not be used directly, but via the Cache wrapper API.
-
#
-
# key - String cache key.
-
#
-
# Returns Object or nil or the value is not set.
-
1
def get(key)
-
40
exists = true
-
74
value = @cache.delete(key) { exists = false }
-
40
if exists
-
6
@cache[key] = value
-
else
-
nil
-
end
-
end
-
-
# Public: Set a key and value in the cache.
-
#
-
# This API should not be used directly, but via the Cache wrapper API.
-
#
-
# key - String cache key.
-
# value - Object value.
-
#
-
# Returns Object value.
-
1
def set(key, value)
-
58
@cache.delete(key)
-
58
@cache[key] = value
-
58
@cache.shift if @cache.size > @max_size
-
58
value
-
end
-
-
# Public: Pretty inspect
-
#
-
# Returns String.
-
1
def inspect
-
"#<#{self.class} size=#{@cache.size}/#{@max_size}>"
-
end
-
-
# Public: Clear the cache
-
#
-
# Returns true
-
1
def clear(options=nil)
-
@cache.clear
-
true
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/base'
-
-
1
module Sprockets
-
# `CachedEnvironment` is a special cached version of `Environment`.
-
#
-
# The expection is that all of its file system methods are cached
-
# for the instances lifetime. This makes `CachedEnvironment` much faster. This
-
# behavior is ideal in production environments where the file system
-
# is immutable.
-
#
-
# `CachedEnvironment` should not be initialized directly. Instead use
-
# `Environment#cached`.
-
1
class CachedEnvironment < Base
-
1
def initialize(environment)
-
2
initialize_configuration(environment)
-
-
2
@cache = environment.cache
-
2
@stats = {}
-
2
@entries = {}
-
2
@uris = {}
-
2
@processor_cache_keys = {}
-
2
@resolved_dependencies = {}
-
end
-
-
# No-op return self as cached environment.
-
1
def cached
-
self
-
end
-
1
alias_method :index, :cached
-
-
# Internal: Cache Environment#entries
-
1
def entries(path)
-
7
@entries[path] ||= super(path)
-
end
-
-
# Internal: Cache Environment#stat
-
1
def stat(path)
-
50
@stats[path] ||= super(path)
-
end
-
-
# Internal: Cache Environment#load
-
1
def load(uri)
-
18
@uris[uri] ||= super(uri)
-
end
-
-
# Internal: Cache Environment#processor_cache_key
-
1
def processor_cache_key(str)
-
13
@processor_cache_keys[str] ||= super(str)
-
end
-
-
# Internal: Cache Environment#resolve_dependency
-
1
def resolve_dependency(str)
-
53
@resolved_dependencies[str] ||= super(str)
-
end
-
-
1
private
-
# Cache is immutable, any methods that try to change the runtime config
-
# should bomb.
-
1
def config=(config)
-
raise RuntimeError, "can't modify immutable cached environment"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/autoload'
-
1
require 'sprockets/digest_utils'
-
-
1
module Sprockets
-
# Public: Closure Compiler minifier.
-
#
-
# To accept the default options
-
#
-
# environment.register_bundle_processor 'application/javascript',
-
# Sprockets::ClosureCompressor
-
#
-
# Or to pass options to the Closure::Compiler class.
-
#
-
# environment.register_bundle_processor 'application/javascript',
-
# Sprockets::ClosureCompressor.new({ ... })
-
#
-
1
class ClosureCompressor
-
1
VERSION = '1'
-
-
# Public: Return singleton instance with default options.
-
#
-
# Returns ClosureCompressor object.
-
1
def self.instance
-
@instance ||= new
-
end
-
-
1
def self.call(input)
-
instance.call(input)
-
end
-
-
1
def self.cache_key
-
instance.cache_key
-
end
-
-
1
attr_reader :cache_key
-
-
1
def initialize(options = {})
-
@options = options
-
@cache_key = "#{self.class.name}:#{Autoload::Closure::VERSION}:#{Autoload::Closure::COMPILER_VERSION}:#{VERSION}:#{DigestUtils.digest(options)}".freeze
-
end
-
-
1
def call(input)
-
@compiler ||= Autoload::Closure::Compiler.new(@options)
-
@compiler.compile(input[:data])
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/autoload'
-
1
require 'sprockets/source_map_utils'
-
-
1
module Sprockets
-
# Processor engine class for the CoffeeScript compiler.
-
# Depends on the `coffee-script` and `coffee-script-source` gems.
-
#
-
# For more infomation see:
-
#
-
# https://github.com/rails/ruby-coffee-script
-
#
-
1
module CoffeeScriptProcessor
-
1
VERSION = '2'
-
-
1
def self.cache_key
-
@cache_key ||= "#{name}:#{Autoload::CoffeeScript::Source.version}:#{VERSION}".freeze
-
end
-
-
1
def self.call(input)
-
data = input[:data]
-
-
js, map = input[:cache].fetch([self.cache_key, data]) do
-
result = Autoload::CoffeeScript.compile(
-
data,
-
sourceMap: "v3",
-
sourceFiles: [File.basename(input[:filename])],
-
generatedFile: input[:filename]
-
)
-
[result['js'], JSON.parse(result['v3SourceMap'])]
-
end
-
-
map = SourceMapUtils.format_source_map(map, input)
-
map = SourceMapUtils.combine_source_maps(input[:metadata][:map], map)
-
-
{ data: js, map: map }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/utils'
-
-
1
module Sprockets
-
# `Compressing` is an internal mixin whose public methods are exposed on
-
# the `Environment` and `CachedEnvironment` classes.
-
1
module Compressing
-
1
include Utils
-
-
1
def compressors
-
config[:compressors]
-
end
-
-
# Public: Register a new compressor `klass` at `sym` for `mime_type`.
-
#
-
# Registering a processor allows it to be looked up by `sym` later when
-
# assigning a JavaScript or CSS compressor.
-
#
-
# Compressors only operate on JavaScript and CSS. If you want to compress a
-
# different type of asset, use a processor instead.
-
#
-
# Examples
-
#
-
# register_compressor 'text/css', :my_sass, MySassCompressor
-
# css_compressor = :my_sass
-
#
-
# mime_type - String MIME Type (one of: 'test/css' or 'application/javascript').
-
# sym - Symbol registration address.
-
# klass - The compressor class.
-
#
-
# Returns nothing.
-
1
def register_compressor(mime_type, sym, klass)
-
11
self.config = hash_reassoc(config, :compressors, mime_type) do |compressors|
-
11
compressors[sym] = klass
-
11
compressors
-
end
-
end
-
-
# Return CSS compressor or nil if none is set
-
1
def css_compressor
-
if defined? @css_compressor
-
@css_compressor
-
end
-
end
-
-
# Assign a compressor to run on `text/css` assets.
-
#
-
# The compressor object must respond to `compress`.
-
1
def css_compressor=(compressor)
-
1
unregister_bundle_processor 'text/css', @css_compressor if defined? @css_compressor
-
1
@css_compressor = nil
-
1
return unless compressor
-
-
1
if compressor.is_a?(Symbol)
-
1
@css_compressor = klass = config[:compressors]['text/css'][compressor] || raise(Error, "unknown compressor: #{compressor}")
-
elsif compressor.respond_to?(:compress)
-
klass = proc { |input| compressor.compress(input[:data]) }
-
@css_compressor = :css_compressor
-
else
-
@css_compressor = klass = compressor
-
end
-
-
1
register_bundle_processor 'text/css', klass
-
end
-
-
# Return JS compressor or nil if none is set
-
1
def js_compressor
-
if defined? @js_compressor
-
@js_compressor
-
end
-
end
-
-
# Assign a compressor to run on `application/javascript` assets.
-
#
-
# The compressor object must respond to `compress`.
-
1
def js_compressor=(compressor)
-
1
unregister_bundle_processor 'application/javascript', @js_compressor if defined? @js_compressor
-
1
@js_compressor = nil
-
1
return unless compressor
-
-
1
if compressor.is_a?(Symbol)
-
@js_compressor = klass = config[:compressors]['application/javascript'][compressor] || raise(Error, "unknown compressor: #{compressor}")
-
1
elsif compressor.respond_to?(:compress)
-
2
klass = proc { |input| compressor.compress(input[:data]) }
-
1
@js_compressor = :js_compressor
-
else
-
@js_compressor = klass = compressor
-
end
-
-
1
register_bundle_processor 'application/javascript', klass
-
end
-
-
# Public: Checks if Gzip is enabled.
-
1
def gzip?
-
config[:gzip_enabled]
-
end
-
-
# Public: Checks if Gzip is disabled.
-
1
def skip_gzip?
-
!gzip?
-
end
-
-
# Public: Enable or disable the creation of Gzip files.
-
#
-
# To disable gzip generation set to a falsey value:
-
#
-
# environment.gzip = false
-
#
-
# To enable set to a truthy value. By default zlib wil
-
# be used to gzip assets. If you have the Zopfli gem
-
# installed you can specify the zopfli algorithm to be used
-
# instead:
-
#
-
# environment.gzip = :zopfli
-
#
-
1
def gzip=(gzip)
-
self.config = config.merge(gzip_enabled: gzip).freeze
-
-
case gzip
-
when false, nil
-
self.unregister_exporter Exporters::ZlibExporter
-
self.unregister_exporter Exporters::ZopfliExporter
-
when :zopfli
-
self.unregister_exporter Exporters::ZlibExporter
-
self.register_exporter '*/*', Exporters::ZopfliExporter
-
else
-
self.unregister_exporter Exporters::ZopfliExporter
-
self.register_exporter '*/*', Exporters::ZlibExporter
-
end
-
-
gzip
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/compressing'
-
1
require 'sprockets/dependencies'
-
1
require 'sprockets/mime'
-
1
require 'sprockets/paths'
-
1
require 'sprockets/processing'
-
1
require 'sprockets/exporting'
-
1
require 'sprockets/transformers'
-
1
require 'sprockets/utils'
-
-
1
module Sprockets
-
1
module Configuration
-
1
include Paths, Mime, Transformers, Processing, Exporting, Compressing, Dependencies, Utils
-
-
1
def initialize_configuration(parent)
-
3
@config = parent.config
-
3
@logger = parent.logger
-
3
@context_class = Class.new(parent.context_class)
-
end
-
-
1
attr_reader :config
-
-
1
def config=(config)
-
242
raise TypeError, "can't assign mutable config" unless config.frozen?
-
242
@config = config
-
end
-
-
# Get and set `Logger` instance.
-
1
attr_accessor :logger
-
-
# The `Environment#version` is a custom value used for manually
-
# expiring all asset caches.
-
#
-
# Sprockets is able to track most file and directory changes and
-
# will take care of expiring the cache for you. However, its
-
# impossible to know when any custom helpers change that you mix
-
# into the `Context`.
-
#
-
# It would be wise to increment this value anytime you make a
-
# configuration change to the `Environment` object.
-
1
def version
-
8
config[:version]
-
end
-
-
# Assign an environment version.
-
#
-
# environment.version = '2.0'
-
#
-
1
def version=(version)
-
self.config = hash_reassoc(config, :version) { version.dup }
-
end
-
-
# Public: Returns a `Digest` implementation class.
-
#
-
# Defaults to `Digest::SHA256`.
-
1
def digest_class
-
26
config[:digest_class]
-
end
-
-
# Deprecated: Assign a `Digest` implementation class. This maybe any Ruby
-
# `Digest::` implementation such as `Digest::SHA256` or
-
# `Digest::MD5`.
-
#
-
# environment.digest_class = Digest::MD5
-
#
-
1
def digest_class=(klass)
-
self.config = config.merge(digest_class: klass).freeze
-
end
-
-
# This class maybe mutated and mixed in with custom helpers.
-
#
-
# environment.context_class.instance_eval do
-
# include MyHelpers
-
# def asset_url; end
-
# end
-
#
-
1
attr_reader :context_class
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'rack/utils'
-
1
require 'set'
-
1
require 'sprockets/errors'
-
-
1
module Sprockets
-
# They are typically accessed by ERB templates. You can mix in custom helpers
-
# by injecting them into `Environment#context_class`. Do not mix them into
-
# `Context` directly.
-
#
-
# environment.context_class.class_eval do
-
# include MyHelper
-
# def asset_url; end
-
# end
-
#
-
# <%= asset_url "foo.png" %>
-
#
-
# The `Context` also collects dependencies declared by
-
# assets. See `DirectiveProcessor` for an example of this.
-
1
class Context
-
# Internal: Proxy for ENV that keeps track of the environment variables used
-
1
class ENVProxy < SimpleDelegator
-
1
def initialize(context)
-
@context = context
-
super(ENV)
-
end
-
-
1
def [](key)
-
@context.depend_on_env(key)
-
super
-
end
-
-
1
def fetch(key, *)
-
@context.depend_on_env(key)
-
super
-
end
-
end
-
-
1
attr_reader :environment, :filename
-
-
1
def initialize(input)
-
1
@environment = input[:environment]
-
1
@metadata = input[:metadata]
-
1
@load_path = input[:load_path]
-
1
@logical_path = input[:name]
-
1
@filename = input[:filename]
-
1
@dirname = File.dirname(@filename)
-
1
@content_type = input[:content_type]
-
-
1
@required = Set.new(@metadata[:required])
-
1
@stubbed = Set.new(@metadata[:stubbed])
-
1
@links = Set.new(@metadata[:links])
-
1
@dependencies = Set.new(input[:metadata][:dependencies])
-
end
-
-
1
def metadata
-
10
{ required: @required,
-
stubbed: @stubbed,
-
links: @links,
-
dependencies: @dependencies }
-
end
-
-
1
def env_proxy
-
ENVProxy.new(self)
-
end
-
-
# Returns the environment path that contains the file.
-
#
-
# If `app/javascripts` and `app/stylesheets` are in your path, and
-
# current file is `app/javascripts/foo/bar.js`, `load_path` would
-
# return `app/javascripts`.
-
1
attr_reader :load_path
-
1
alias_method :root_path, :load_path
-
-
# Returns logical path without any file extensions.
-
#
-
# 'app/javascripts/application.js'
-
# # => 'application'
-
#
-
1
attr_reader :logical_path
-
-
# Returns content type of file
-
#
-
# 'application/javascript'
-
# 'text/css'
-
#
-
1
attr_reader :content_type
-
-
# Public: Given a logical path, `resolve` will find and return an Asset URI.
-
# Relative paths will also be resolved. An accept type maybe given to
-
# restrict the search.
-
#
-
# resolve("foo.js")
-
# # => "file:///path/to/app/javascripts/foo.js?type=application/javascript"
-
#
-
# resolve("./bar.js")
-
# # => "file:///path/to/app/javascripts/bar.js?type=application/javascript"
-
#
-
# path - String logical or absolute path
-
# accept - String content accept type
-
#
-
# Returns an Asset URI String.
-
1
def resolve(path, **kargs)
-
kargs[:base_path] = @dirname
-
uri, deps = environment.resolve!(path, **kargs)
-
@dependencies.merge(deps)
-
uri
-
end
-
-
# Public: Load Asset by AssetURI and track it as a dependency.
-
#
-
# uri - AssetURI
-
#
-
# Returns Asset.
-
1
def load(uri)
-
asset = environment.load(uri)
-
@dependencies.merge(asset.metadata[:dependencies])
-
asset
-
end
-
-
# `depend_on` allows you to state a dependency on a file without
-
# including it.
-
#
-
# This is used for caching purposes. Any changes made to
-
# the dependency file will invalidate the cache of the
-
# source file.
-
1
def depend_on(path)
-
if environment.absolute_path?(path) && environment.stat(path)
-
@dependencies << environment.build_file_digest_uri(path)
-
else
-
resolve(path)
-
end
-
nil
-
end
-
-
# `depend_on_asset` allows you to state an asset dependency
-
# without including it.
-
#
-
# This is used for caching purposes. Any changes that would
-
# invalidate the dependency asset will invalidate the source
-
# file. Unlike `depend_on`, this will recursively include
-
# the target asset's dependencies.
-
1
def depend_on_asset(path)
-
load(resolve(path))
-
end
-
-
# `depend_on_env` allows you to state a dependency on an environment
-
# variable.
-
#
-
# This is used for caching purposes. Any changes in the value of the
-
# environment variable will invalidate the cache of the source file.
-
1
def depend_on_env(key)
-
@dependencies << "env:#{key}"
-
end
-
-
# `require_asset` declares `path` as a dependency of the file. The
-
# dependency will be inserted before the file and will only be
-
# included once.
-
#
-
# If ERB processing is enabled, you can use it to dynamically
-
# require assets.
-
#
-
# <%= require_asset "#{framework}.js" %>
-
#
-
1
def require_asset(path)
-
@required << resolve(path, accept: @content_type, pipeline: :self)
-
nil
-
end
-
-
# `stub_asset` blacklists `path` from being included in the bundle.
-
# `path` must be an asset which may or may not already be included
-
# in the bundle.
-
1
def stub_asset(path)
-
@stubbed << resolve(path, accept: @content_type, pipeline: :self)
-
nil
-
end
-
-
# `link_asset` declares an external dependency on an asset without directly
-
# including it. The target asset is returned from this function making it
-
# easy to construct a link to it.
-
#
-
# Returns an Asset or nil.
-
1
def link_asset(path)
-
asset = depend_on_asset(path)
-
@links << asset.uri
-
asset
-
end
-
-
# Returns a `data:` URI with the contents of the asset at the specified
-
# path, and marks that path as a dependency of the current file.
-
#
-
# Uses URI encoding for SVG files, base64 encoding for all the other files.
-
#
-
# Use `asset_data_uri` from ERB with CSS or JavaScript assets:
-
#
-
# #logo { background: url(<%= asset_data_uri 'logo.png' %>) }
-
#
-
# $('<img>').attr('src', '<%= asset_data_uri 'avatar.jpg' %>')
-
#
-
1
def asset_data_uri(path)
-
asset = depend_on_asset(path)
-
if asset.content_type == 'image/svg+xml'
-
svg_asset_data_uri(asset)
-
else
-
base64_asset_data_uri(asset)
-
end
-
end
-
-
# Expands logical path to full url to asset.
-
#
-
# NOTE: This helper is currently not implemented and should be
-
# customized by the application. Though, in the future, some
-
# basics implemention may be provided with different methods that
-
# are required to be overridden.
-
1
def asset_path(path, options = {})
-
message = <<-EOS
-
Custom asset_path helper is not implemented
-
-
Extend your environment context with a custom method.
-
-
environment.context_class.class_eval do
-
def asset_path(path, options = {})
-
end
-
end
-
EOS
-
raise NotImplementedError, message
-
end
-
-
# Expand logical image asset path.
-
1
def image_path(path)
-
asset_path(path, type: :image)
-
end
-
-
# Expand logical video asset path.
-
1
def video_path(path)
-
asset_path(path, type: :video)
-
end
-
-
# Expand logical audio asset path.
-
1
def audio_path(path)
-
asset_path(path, type: :audio)
-
end
-
-
# Expand logical font asset path.
-
1
def font_path(path)
-
asset_path(path, type: :font)
-
end
-
-
# Expand logical javascript asset path.
-
1
def javascript_path(path)
-
asset_path(path, type: :javascript)
-
end
-
-
# Expand logical stylesheet asset path.
-
1
def stylesheet_path(path)
-
asset_path(path, type: :stylesheet)
-
end
-
-
1
protected
-
-
# Returns a URI-encoded data URI (always "-quoted).
-
1
def svg_asset_data_uri(asset)
-
svg = asset.source.dup
-
optimize_svg_for_uri_escaping!(svg)
-
data = Rack::Utils.escape(svg)
-
optimize_quoted_uri_escapes!(data)
-
"\"data:#{asset.content_type};charset=utf-8,#{data}\""
-
end
-
-
# Returns a Base64-encoded data URI.
-
1
def base64_asset_data_uri(asset)
-
data = Rack::Utils.escape(EncodingUtils.base64(asset.source))
-
"data:#{asset.content_type};base64,#{data}"
-
end
-
-
# Optimizes an SVG for being URI-escaped.
-
#
-
# This method only performs these basic but crucial optimizations:
-
# * Replaces " with ', because ' does not need escaping.
-
# * Removes comments, meta, doctype, and newlines.
-
# * Collapses whitespace.
-
1
def optimize_svg_for_uri_escaping!(svg)
-
# Remove comments, xml meta, and doctype
-
svg.gsub!(/<!--.*?-->|<\?.*?\?>|<!.*?>/m, '')
-
# Replace consecutive whitespace and newlines with a space
-
svg.gsub!(/\s+/, ' ')
-
# Collapse inter-tag whitespace
-
svg.gsub!('> <', '><')
-
# Replace " with '
-
svg.gsub!(/([\w:])="(.*?)"/, "\\1='\\2'")
-
svg.strip!
-
end
-
-
# Un-escapes characters in the given URI-escaped string that do not need
-
# escaping in "-quoted data URIs.
-
1
def optimize_quoted_uri_escapes!(escaped)
-
escaped.gsub!('%3D', '=')
-
escaped.gsub!('%3A', ':')
-
escaped.gsub!('%2F', '/')
-
escaped.gsub!('%27', "'")
-
escaped.tr!('+', ' ')
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/digest_utils'
-
1
require 'sprockets/path_digest_utils'
-
1
require 'sprockets/uri_utils'
-
-
1
module Sprockets
-
# `Dependencies` is an internal mixin whose public methods are exposed on the
-
# `Environment` and `CachedEnvironment` classes.
-
1
module Dependencies
-
1
include DigestUtils, PathDigestUtils, URIUtils
-
-
# Public: Mapping dependency schemes to resolver functions.
-
#
-
# key - String scheme
-
# value - Proc.call(Environment, String)
-
#
-
# Returns Hash.
-
1
def dependency_resolvers
-
config[:dependency_resolvers]
-
end
-
-
# Public: Default set of dependency URIs for assets.
-
#
-
# Returns Set of String URIs.
-
1
def dependencies
-
config[:dependencies]
-
end
-
-
# Public: Register new dependency URI resolver.
-
#
-
# scheme - String scheme
-
# block -
-
# environment - Environment
-
# uri - String dependency URI
-
#
-
# Returns nothing.
-
1
def register_dependency_resolver(scheme, &block)
-
5
self.config = hash_reassoc(config, :dependency_resolvers) do |hash|
-
5
hash.merge(scheme => block)
-
end
-
end
-
-
# Public: Add environmental dependency inheirted by all assets.
-
#
-
# uri - String dependency URI
-
#
-
# Returns nothing.
-
1
def add_dependency(uri)
-
2
self.config = hash_reassoc(config, :dependencies) do |set|
-
2
set + Set.new([uri])
-
end
-
end
-
1
alias_method :depend_on, :add_dependency
-
-
# Internal: Resolve dependency URIs.
-
#
-
# Returns resolved Object.
-
1
def resolve_dependency(str)
-
# Optimize for the most common scheme to
-
# save 22k allocations on an average Spree app.
-
25
scheme = if str.start_with?('file-digest:'.freeze)
-
16
'file-digest'.freeze
-
else
-
9
str[/([^:]+)/, 1]
-
end
-
-
25
if resolver = config[:dependency_resolvers][scheme]
-
25
resolver.call(self, str)
-
else
-
nil
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'digest/md5'
-
1
require 'digest/sha1'
-
1
require 'digest/sha2'
-
1
require 'set'
-
-
1
module Sprockets
-
# Internal: Hash functions and digest related utilities. Mixed into
-
# Environment.
-
1
module DigestUtils
-
1
extend self
-
-
# Internal: Default digest class.
-
#
-
# Returns a Digest::Base subclass.
-
1
def digest_class
-
50
Digest::SHA256
-
end
-
-
# Internal: Maps digest bytesize to the digest class.
-
DIGEST_SIZES = {
-
1
16 => Digest::MD5,
-
20 => Digest::SHA1,
-
32 => Digest::SHA256,
-
48 => Digest::SHA384,
-
64 => Digest::SHA512
-
}
-
-
# Internal: Detect digest class hash algorithm for digest bytes.
-
#
-
# While not elegant, all the supported digests have a unique bytesize.
-
#
-
# Returns Digest::Base or nil.
-
1
def detect_digest_class(bytes)
-
DIGEST_SIZES[bytes.bytesize]
-
end
-
-
ADD_VALUE_TO_DIGEST = {
-
316
String => ->(val, digest) { digest << val },
-
FalseClass => ->(val, digest) { digest << 'FalseClass'.freeze },
-
TrueClass => ->(val, digest) { digest << 'TrueClass'.freeze },
-
29
NilClass => ->(val, digest) { digest << 'NilClass'.freeze },
-
-
Symbol => ->(val, digest) {
-
117
digest << 'Symbol'.freeze
-
117
digest << val.to_s
-
},
-
Integer => ->(val, digest) {
-
23
digest << 'Integer'.freeze
-
23
digest << val.to_s
-
},
-
Array => ->(val, digest) {
-
239
digest << 'Array'.freeze
-
239
val.each do |element|
-
517
ADD_VALUE_TO_DIGEST[element.class].call(element, digest)
-
end
-
},
-
Hash => ->(val, digest) {
-
24
digest << 'Hash'.freeze
-
24
val.sort.each do |array|
-
168
ADD_VALUE_TO_DIGEST[Array].call(array, digest)
-
end
-
},
-
Set => ->(val, digest) {
-
29
digest << 'Set'.freeze
-
29
ADD_VALUE_TO_DIGEST[Array].call(val, digest)
-
},
-
Encoding => ->(val, digest) {
-
digest << 'Encoding'.freeze
-
digest << val.name
-
},
-
}
-
1
if 0.class != Integer # Ruby < 2.4
-
ADD_VALUE_TO_DIGEST[Fixnum] = ->(val, digest) {
-
digest << 'Integer'.freeze
-
digest << val.to_s
-
}
-
ADD_VALUE_TO_DIGEST[Bignum] = ->(val, digest) {
-
digest << 'Integer'.freeze
-
digest << val.to_s
-
}
-
end
-
-
1
ADD_VALUE_TO_DIGEST.compare_by_identity.rehash
-
-
1
ADD_VALUE_TO_DIGEST.default_proc = ->(_, val) {
-
raise TypeError, "couldn't digest #{ val }"
-
}
-
1
private_constant :ADD_VALUE_TO_DIGEST
-
-
# Internal: Generate a hexdigest for a nested JSON serializable object.
-
#
-
# This is used for generating cache keys, so its pretty important its
-
# wicked fast. Microbenchmarks away!
-
#
-
# obj - A JSON serializable object.
-
#
-
# Returns a String digest of the object.
-
1
def digest(obj)
-
56
build_digest(obj).digest
-
end
-
-
# Internal: Generate a hexdigest for a nested JSON serializable object.
-
#
-
# The same as `pack_hexdigest(digest(obj))`.
-
#
-
# obj - A JSON serializable object.
-
#
-
# Returns a String digest of the object.
-
1
def hexdigest(obj)
-
6
build_digest(obj).hexdigest!
-
end
-
-
# Internal: Pack a binary digest to a hex encoded string.
-
#
-
# bin - String bytes
-
#
-
# Returns hex String.
-
1
def pack_hexdigest(bin)
-
2
bin.unpack('H*'.freeze).first
-
end
-
-
# Internal: Unpack a hex encoded digest string into binary bytes.
-
#
-
# hex - String hex
-
#
-
# Returns binary String.
-
1
def unpack_hexdigest(hex)
-
[hex].pack('H*')
-
end
-
-
# Internal: Pack a binary digest to a base64 encoded string.
-
#
-
# bin - String bytes
-
#
-
# Returns base64 String.
-
1
def pack_base64digest(bin)
-
44
[bin].pack('m0')
-
end
-
-
# Internal: Pack a binary digest to a urlsafe base64 encoded string.
-
#
-
# bin - String bytes
-
#
-
# Returns urlsafe base64 String.
-
1
def pack_urlsafe_base64digest(bin)
-
44
str = pack_base64digest(bin)
-
44
str.tr!('+/'.freeze, '-_'.freeze)
-
44
str.tr!('='.freeze, ''.freeze)
-
44
str
-
end
-
-
# Internal: Maps digest class to the CSP hash algorithm name.
-
HASH_ALGORITHMS = {
-
1
Digest::SHA256 => 'sha256'.freeze,
-
Digest::SHA384 => 'sha384'.freeze,
-
Digest::SHA512 => 'sha512'.freeze
-
}
-
-
# Public: Generate hash for use in the `integrity` attribute of an asset tag
-
# as per the subresource integrity specification.
-
#
-
# digest - The String byte digest of the asset content.
-
#
-
# Returns a String or nil if hash algorithm is incompatible.
-
1
def integrity_uri(digest)
-
case digest
-
when Digest::Base
-
digest_class = digest.class
-
digest = digest.digest
-
when String
-
digest_class = DIGEST_SIZES[digest.bytesize]
-
else
-
raise TypeError, "unknown digest: #{digest.inspect}"
-
end
-
-
if hash_name = HASH_ALGORITHMS[digest_class]
-
"#{hash_name}-#{pack_base64digest(digest)}"
-
end
-
end
-
-
# Public: Generate hash for use in the `integrity` attribute of an asset tag
-
# as per the subresource integrity specification.
-
#
-
# digest - The String hexbyte digest of the asset content.
-
#
-
# Returns a String or nil if hash algorithm is incompatible.
-
1
def hexdigest_integrity_uri(hexdigest)
-
integrity_uri(unpack_hexdigest(hexdigest))
-
end
-
-
1
private
-
1
def build_digest(obj)
-
62
digest = digest_class.new
-
-
62
ADD_VALUE_TO_DIGEST[obj.class].call(obj, digest)
-
62
digest
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'set'
-
1
require 'shellwords'
-
-
1
module Sprockets
-
# The `DirectiveProcessor` is responsible for parsing and evaluating
-
# directive comments in a source file.
-
#
-
# A directive comment starts with a comment prefix, followed by an "=",
-
# then the directive name, then any arguments.
-
#
-
# // JavaScript
-
# //= require "foo"
-
#
-
# # CoffeeScript
-
# #= require "bar"
-
#
-
# /* CSS
-
# *= require "baz"
-
# */
-
#
-
# This makes it possible to disable or modify the processor to do whatever
-
# you'd like. You could add your own custom directives or invent your own
-
# directive syntax.
-
#
-
# `Environment#processors` includes `DirectiveProcessor` by default.
-
#
-
# To remove the processor entirely:
-
#
-
# env.unregister_processor('text/css', Sprockets::DirectiveProcessor)
-
# env.unregister_processor('application/javascript', Sprockets::DirectiveProcessor)
-
#
-
# Then inject your own preprocessor:
-
#
-
# env.register_processor('text/css', MyProcessor)
-
#
-
1
class DirectiveProcessor
-
# Directives are denoted by a `=` followed by the name, then
-
# argument list.
-
#
-
# A few different styles are allowed:
-
#
-
# // =require foo
-
# //= require foo
-
# //= require "foo"
-
#
-
1
DIRECTIVE_PATTERN = /
-
^ \W* = \s* (\w+.*?) (\*\/)? $
-
/x
-
-
1
def self.instance
-
# Default to C comment styles
-
@instance ||= new(comments: ["//", ["/*", "*/"]])
-
end
-
-
1
def self.call(input)
-
instance.call(input)
-
end
-
-
1
def initialize(comments: [])
-
6
@header_pattern = compile_header_pattern(Array(comments))
-
end
-
-
1
def call(input)
-
5
dup._call(input)
-
end
-
-
1
def _call(input)
-
5
@environment = input[:environment]
-
5
@uri = input[:uri]
-
5
@filename = input[:filename]
-
5
@dirname = File.dirname(@filename)
-
# If loading a source map file like `application.js.map` resolve
-
# dependencies using `.js` instead of `.js.map`
-
5
@content_type = SourceMapProcessor.original_content_type(input[:content_type], error_when_not_found: false)
-
5
@required = Set.new(input[:metadata][:required])
-
5
@stubbed = Set.new(input[:metadata][:stubbed])
-
5
@links = Set.new(input[:metadata][:links])
-
5
@dependencies = Set.new(input[:metadata][:dependencies])
-
5
@to_link = Set.new
-
5
@to_load = Set.new
-
-
5
data, directives = process_source(input[:data])
-
5
process_directives(directives)
-
-
{
-
5
data: data,
-
required: @required,
-
stubbed: @stubbed,
-
links: @links,
-
to_load: @to_load,
-
to_link: @to_link,
-
dependencies: @dependencies
-
}
-
end
-
-
1
protected
-
# Directives will only be picked up if they are in the header
-
# of the source file. C style (/* */), JavaScript (//), and
-
# Ruby (#) comments are supported.
-
#
-
# Directives in comments after the first non-whitespace line
-
# of code will not be processed.
-
1
def compile_header_pattern(comments)
-
6
re = comments.map { |c|
-
12
case c
-
when String
-
6
"(?:#{Regexp.escape(c)}.*\\n?)+"
-
when Array
-
6
"(?:#{Regexp.escape(c[0])}(?m:.*?)#{Regexp.escape(c[1])})"
-
else
-
raise TypeError, "unknown comment type: #{c.class}"
-
end
-
}.join("|")
-
6
Regexp.compile("\\A(?:(?m:\\s*)(?:#{re}))+")
-
end
-
-
1
def process_source(source)
-
5
header = source[@header_pattern, 0] || ""
-
5
body = $' || source
-
-
5
header, directives = extract_directives(header)
-
-
5
data = +""
-
5
data.force_encoding(body.encoding)
-
5
data << header unless header.empty?
-
5
data << body
-
# Ensure body ends in a new line
-
5
data << "\n" if data.length > 0 && data[-1] != "\n"
-
-
5
return data, directives
-
end
-
-
# Returns an Array of directive structures. Each structure
-
# is an Array with the line number as the first element, the
-
# directive name as the second element, followed by any
-
# arguments.
-
#
-
# [[1, "require", "foo"], [2, "require", "bar"]]
-
#
-
1
def extract_directives(header)
-
5
processed_header = +""
-
5
directives = []
-
-
5
header.lines.each_with_index do |line, index|
-
2
if directive = line[DIRECTIVE_PATTERN, 1]
-
2
name, *args = Shellwords.shellwords(directive)
-
2
if respond_to?("process_#{name}_directive", true)
-
2
directives << [index + 1, name, *args]
-
# Replace directive line with a clean break
-
2
line = "\n"
-
end
-
end
-
2
processed_header << line
-
end
-
-
5
processed_header.chomp!
-
# Ensure header ends in a new line like before it was processed
-
5
processed_header << "\n" if processed_header.length > 0 && header[-1] == "\n"
-
-
5
return processed_header, directives
-
end
-
-
# Gathers comment directives in the source and processes them.
-
# Any directive method matching `process_*_directive` will
-
# automatically be available. This makes it easy to extend the
-
# processor.
-
#
-
# To implement a custom directive called `require_glob`, subclass
-
# `Sprockets::DirectiveProcessor`, then add a method called
-
# `process_require_glob_directive`.
-
#
-
# class DirectiveProcessor < Sprockets::DirectiveProcessor
-
# def process_require_glob_directive(glob)
-
# Dir["#{dirname}/#{glob}"].sort.each do |filename|
-
# require(filename)
-
# end
-
# end
-
# end
-
#
-
# Replace the current processor on the environment with your own:
-
#
-
# env.unregister_processor('text/css', Sprockets::DirectiveProcessor)
-
# env.register_processor('text/css', DirectiveProcessor)
-
#
-
1
def process_directives(directives)
-
5
directives.each do |line_number, name, *args|
-
begin
-
2
send("process_#{name}_directive", *args)
-
rescue Exception => e
-
e.set_backtrace(["#{@filename}:#{line_number}"] + e.backtrace)
-
raise e
-
end
-
end
-
end
-
-
# The `require` directive functions similar to Ruby's own `require`.
-
# It provides a way to declare a dependency on a file in your path
-
# and ensures it's only loaded once before the source file.
-
#
-
# `require` works with files in the environment path:
-
#
-
# //= require "foo.js"
-
#
-
# Extensions are optional. If your source file is ".js", it
-
# assumes you are requiring another ".js".
-
#
-
# //= require "foo"
-
#
-
# Relative paths work too. Use a leading `./` to denote a relative
-
# path:
-
#
-
# //= require "./bar"
-
#
-
1
def process_require_directive(path)
-
1
@required << resolve(path, accept: @content_type, pipeline: :self)
-
end
-
-
# `require_self` causes the body of the current file to be inserted
-
# before any subsequent `require` directives. Useful in CSS files, where
-
# it's common for the index file to contain global styles that need to
-
# be defined before other dependencies are loaded.
-
#
-
# /*= require "reset"
-
# *= require_self
-
# *= require_tree .
-
# */
-
#
-
1
def process_require_self_directive
-
if @required.include?(@uri)
-
raise ArgumentError, "require_self can only be called once per source file"
-
end
-
@required << @uri
-
end
-
-
# `require_directory` requires all the files inside a single
-
# directory. It's similar to `path/*` since it does not follow
-
# nested directories.
-
#
-
# //= require_directory "./javascripts"
-
#
-
1
def process_require_directory_directive(path = ".")
-
path = expand_relative_dirname(:require_directory, path)
-
require_paths(*@environment.stat_directory_with_dependencies(path))
-
end
-
-
# `require_tree` requires all the nested files in a directory.
-
# Its glob equivalent is `path/**/*`.
-
#
-
# //= require_tree "./public"
-
#
-
1
def process_require_tree_directive(path = ".")
-
1
path = expand_relative_dirname(:require_tree, path)
-
1
require_paths(*@environment.stat_sorted_tree_with_dependencies(path))
-
end
-
-
# Allows you to state a dependency on a file without
-
# including it.
-
#
-
# This is used for caching purposes. Any changes made to
-
# the dependency file will invalidate the cache of the
-
# source file.
-
#
-
# This is useful if you are using ERB and File.read to pull
-
# in contents from another file.
-
#
-
# //= depend_on "foo.png"
-
#
-
1
def process_depend_on_directive(path)
-
resolve(path)
-
end
-
-
# Allows you to state a dependency on an asset without including
-
# it.
-
#
-
# This is used for caching purposes. Any changes that would
-
# invalidate the asset dependency will invalidate the cache of
-
# the source file.
-
#
-
# Unlike `depend_on`, the path must be a requirable asset.
-
#
-
# //= depend_on_asset "bar.js"
-
#
-
1
def process_depend_on_asset_directive(path)
-
to_load(resolve(path))
-
end
-
-
# Allows dependency to be excluded from the asset bundle.
-
#
-
# The `path` must be a valid asset and may or may not already
-
# be part of the bundle. Once stubbed, it is blacklisted and
-
# can't be brought back by any other `require`.
-
#
-
# //= stub "jquery"
-
#
-
1
def process_stub_directive(path)
-
@stubbed << resolve(path, accept: @content_type, pipeline: :self)
-
end
-
-
# Declares a linked dependency on the target asset.
-
#
-
# The `path` must be a valid asset and should not already be part of the
-
# bundle. Any linked assets will automatically be compiled along with the
-
# current.
-
#
-
# /*= link "logo.png" */
-
#
-
1
def process_link_directive(path)
-
uri = to_load(resolve(path))
-
@to_link << uri
-
end
-
-
# `link_directory` links all the files inside a single
-
# directory. It's similar to `path/*` since it does not follow
-
# nested directories.
-
#
-
# //= link_directory "./fonts"
-
#
-
# Use caution when linking against JS or CSS assets. Include an explicit
-
# extension or content type in these cases.
-
#
-
# //= link_directory "./scripts" .js
-
#
-
1
def process_link_directory_directive(path = ".", accept = nil)
-
path = expand_relative_dirname(:link_directory, path)
-
accept = expand_accept_shorthand(accept)
-
link_paths(*@environment.stat_directory_with_dependencies(path), accept)
-
end
-
-
# `link_tree` links all the nested files in a directory.
-
# Its glob equivalent is `path/**/*`.
-
#
-
# //= link_tree "./images"
-
#
-
# Use caution when linking against JS or CSS assets. Include an explicit
-
# extension or content type in these cases.
-
#
-
# //= link_tree "./styles" .css
-
#
-
1
def process_link_tree_directive(path = ".", accept = nil)
-
path = expand_relative_dirname(:link_tree, path)
-
accept = expand_accept_shorthand(accept)
-
link_paths(*@environment.stat_sorted_tree_with_dependencies(path), accept)
-
end
-
-
1
private
-
1
def expand_accept_shorthand(accept)
-
if accept.nil?
-
nil
-
elsif accept.include?("/")
-
accept
-
elsif accept.start_with?(".")
-
@environment.mime_exts[accept]
-
else
-
@environment.mime_exts[".#{accept}"]
-
end
-
end
-
-
1
def require_paths(paths, deps)
-
1
resolve_paths(paths, deps, accept: @content_type, pipeline: :self) do |uri|
-
1
@required << uri
-
end
-
end
-
-
1
def link_paths(paths, deps, accept)
-
resolve_paths(paths, deps, accept: accept) do |uri|
-
@to_link << to_load(uri)
-
end
-
end
-
-
1
def resolve_paths(paths, deps, **kargs)
-
1
@dependencies.merge(deps)
-
1
paths.each do |subpath, stat|
-
2
next if subpath == @filename || stat.directory?
-
1
uri, deps = @environment.resolve(subpath, **kargs)
-
1
@dependencies.merge(deps)
-
1
yield uri if uri
-
end
-
end
-
-
1
def expand_relative_dirname(directive, path)
-
1
if @environment.relative_path?(path)
-
1
path = File.expand_path(path, @dirname)
-
1
stat = @environment.stat(path)
-
-
1
if stat && stat.directory?
-
1
path
-
else
-
raise ArgumentError, "#{directive} argument must be a directory"
-
end
-
else
-
# The path must be relative and start with a `./`.
-
raise ArgumentError, "#{directive} argument must be a relative path"
-
end
-
end
-
-
1
def to_load(uri)
-
@to_load << uri
-
uri
-
end
-
-
1
def resolve(path, **kargs)
-
# Prevent absolute paths in directives
-
1
if @environment.absolute_path?(path)
-
raise FileOutsidePaths, "can't require absolute file: #{path}"
-
end
-
-
1
kargs[:base_path] = @dirname
-
1
uri, deps = @environment.resolve!(path, **kargs)
-
1
@dependencies.merge(deps)
-
1
uri
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/autoload'
-
-
1
module Sprockets
-
# Processor engine class for the Eco compiler. Depends on the `eco` gem.
-
#
-
# For more infomation see:
-
#
-
# https://github.com/sstephenson/ruby-eco
-
# https://github.com/sstephenson/eco
-
#
-
1
module EcoProcessor
-
1
VERSION = '1'
-
-
1
def self.cache_key
-
@cache_key ||= "#{name}:#{Autoload::Eco::Source::VERSION}:#{VERSION}".freeze
-
end
-
-
# Compile template data with Eco compiler.
-
#
-
# Returns a JS function definition String. The result should be
-
# assigned to a JS variable.
-
#
-
# # => "function(...) {...}"
-
#
-
1
def self.call(input)
-
data = input[:data]
-
input[:cache].fetch([cache_key, data]) do
-
Autoload::Eco.compile(data)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/autoload'
-
-
1
module Sprockets
-
# Processor engine class for the EJS compiler. Depends on the `ejs` gem.
-
#
-
# For more infomation see:
-
#
-
# https://github.com/sstephenson/ruby-ejs
-
#
-
1
module EjsProcessor
-
1
VERSION = '1'
-
-
1
def self.cache_key
-
@cache_key ||= "#{name}:#{VERSION}".freeze
-
end
-
-
# Compile template data with EJS compiler.
-
#
-
# Returns a JS function definition String. The result should be
-
# assigned to a JS variable.
-
#
-
# # => "function(obj){...}"
-
#
-
1
def self.call(input)
-
data = input[:data]
-
input[:cache].fetch([cache_key, data]) do
-
Autoload::EJS.compile(data)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'base64'
-
1
require 'stringio'
-
1
require 'zlib'
-
-
1
module Sprockets
-
# Internal: HTTP transport encoding and charset detecting related functions.
-
# Mixed into Environment.
-
1
module EncodingUtils
-
1
extend self
-
-
## Binary encodings ##
-
-
# Public: Use deflate to compress data.
-
#
-
# str - String data
-
#
-
# Returns a compressed String
-
1
def deflate(str)
-
deflater = Zlib::Deflate.new(
-
Zlib::BEST_COMPRESSION,
-
-Zlib::MAX_WBITS,
-
Zlib::MAX_MEM_LEVEL,
-
Zlib::DEFAULT_STRATEGY
-
)
-
deflater << str
-
deflater.finish
-
end
-
-
# Internal: Unmarshal optionally deflated data.
-
#
-
# Checks leading marshal header to see if the bytes are uncompressed
-
# otherwise inflate the data an unmarshal.
-
#
-
# str - Marshaled String
-
# window_bits - Integer deflate window size. See ZLib::Inflate.new()
-
#
-
# Returns unmarshaled Object or raises an Exception.
-
1
def unmarshaled_deflated(str, window_bits = -Zlib::MAX_WBITS)
-
major, minor = str[0], str[1]
-
if major && major.ord == Marshal::MAJOR_VERSION &&
-
minor && minor.ord <= Marshal::MINOR_VERSION
-
marshaled = str
-
else
-
begin
-
marshaled = Zlib::Inflate.new(window_bits).inflate(str)
-
rescue Zlib::DataError
-
marshaled = str
-
end
-
end
-
Marshal.load(marshaled)
-
end
-
-
# Public: Use gzip to compress data.
-
#
-
# str - String data
-
#
-
# Returns a compressed String
-
1
def gzip(str)
-
io = StringIO.new
-
gz = Zlib::GzipWriter.new(io, Zlib::BEST_COMPRESSION)
-
gz.mtime = 1
-
gz << str
-
gz.finish
-
io.string
-
end
-
-
# Public: Use base64 to encode data.
-
#
-
# str - String data
-
#
-
# Returns a encoded String
-
1
def base64(str)
-
Base64.strict_encode64(str)
-
end
-
-
-
## Charset encodings ##
-
-
# Internal: Shorthand aliases for detecter functions.
-
1
CHARSET_DETECT = {}
-
-
# Internal: Mapping unicode encodings to byte order markers.
-
BOM = {
-
1
Encoding::UTF_32LE => [0xFF, 0xFE, 0x00, 0x00],
-
Encoding::UTF_32BE => [0x00, 0x00, 0xFE, 0xFF],
-
Encoding::UTF_8 => [0xEF, 0xBB, 0xBF],
-
Encoding::UTF_16LE => [0xFF, 0xFE],
-
Encoding::UTF_16BE => [0xFE, 0xFF]
-
}
-
-
# Public: Basic string detecter.
-
#
-
# Attempts to parse any Unicode BOM otherwise falls back to the
-
# environment's external encoding.
-
#
-
# str - ASCII-8BIT encoded String
-
#
-
# Returns encoded String.
-
1
def detect(str)
-
str = detect_unicode_bom(str)
-
-
# Attempt Charlock detection
-
if str.encoding == Encoding::BINARY
-
charlock_detect(str)
-
end
-
-
# Fallback to environment's external encoding
-
if str.encoding == Encoding::BINARY
-
str.force_encoding(Encoding.default_external)
-
end
-
-
str
-
end
-
1
CHARSET_DETECT[:default] = method(:detect)
-
-
# Internal: Use Charlock Holmes to detect encoding.
-
#
-
# To enable this code path, require 'charlock_holmes'
-
#
-
# Returns encoded String.
-
1
def charlock_detect(str)
-
if defined? CharlockHolmes::EncodingDetector
-
if detected = CharlockHolmes::EncodingDetector.detect(str)
-
str.force_encoding(detected[:encoding]) if detected[:encoding]
-
end
-
end
-
-
str
-
end
-
-
# Public: Detect Unicode string.
-
#
-
# Attempts to parse Unicode BOM and falls back to UTF-8.
-
#
-
# str - ASCII-8BIT encoded String
-
#
-
# Returns encoded String.
-
1
def detect_unicode(str)
-
2
str = detect_unicode_bom(str)
-
-
# Fallback to UTF-8
-
2
if str.encoding == Encoding::BINARY
-
2
str.force_encoding(Encoding::UTF_8)
-
end
-
-
2
str
-
end
-
1
CHARSET_DETECT[:unicode] = method(:detect_unicode)
-
-
# Public: Detect and strip BOM from possible unicode string.
-
#
-
# str - ASCII-8BIT encoded String
-
#
-
# Returns UTF 8/16/32 encoded String without BOM or the original String if
-
# no BOM was present.
-
1
def detect_unicode_bom(str)
-
4
bom_bytes = str.byteslice(0, 4).bytes.to_a
-
-
4
BOM.each do |encoding, bytes|
-
20
if bom_bytes[0, bytes.size] == bytes
-
str = str.dup
-
str.force_encoding(Encoding::BINARY)
-
str.slice!(0, bytes.size)
-
str.force_encoding(encoding)
-
return str
-
end
-
end
-
-
4
return str
-
end
-
-
# Public: Detect and strip @charset from CSS style sheet.
-
#
-
# str - String.
-
#
-
# Returns a encoded String.
-
1
def detect_css(str)
-
2
str = detect_unicode_bom(str)
-
-
2
if name = scan_css_charset(str)
-
encoding = Encoding.find(name)
-
str = str.dup
-
str.force_encoding(encoding)
-
len = "@charset \"#{name}\";".encode(encoding).size
-
str.slice!(0, len)
-
str
-
end
-
-
# Fallback to UTF-8
-
2
if str.encoding == Encoding::BINARY
-
2
str.force_encoding(Encoding::UTF_8)
-
end
-
-
2
str
-
end
-
1
CHARSET_DETECT[:css] = method(:detect_css)
-
-
# Internal: @charset bytes
-
1
CHARSET_START = [0x40, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x20, 0x22]
-
1
CHARSET_SIZE = CHARSET_START.size
-
-
# Internal: Scan binary CSS string for @charset encoding name.
-
#
-
# str - ASCII-8BIT encoded String
-
#
-
# Returns encoding String name or nil.
-
1
def scan_css_charset(str)
-
2
buf = []
-
2
i = 0
-
-
2
str.each_byte.each do |byte|
-
# Halt on line breaks
-
12
break if byte == 0x0A || byte == 0x0D
-
-
# Only ascii bytes
-
11
next unless 0x0 < byte && byte <= 0xFF
-
-
11
if i < CHARSET_SIZE
-
1
elsif i == CHARSET_SIZE
-
1
if buf == CHARSET_START
-
buf = []
-
else
-
1
break
-
end
-
elsif byte == 0x22
-
return buf.pack('C*')
-
end
-
-
10
buf << byte
-
10
i += 1
-
end
-
-
nil
-
end
-
-
# Public: Detect charset from HTML document.
-
#
-
# Attempts to parse any Unicode BOM otherwise attempt Charlock detection
-
# and finally falls back to the environment's external encoding.
-
#
-
# str - String.
-
#
-
# Returns a encoded String.
-
1
def detect_html(str)
-
str = detect_unicode_bom(str)
-
-
# Attempt Charlock detection
-
if str.encoding == Encoding::BINARY
-
charlock_detect(str)
-
end
-
-
# Fallback to environment's external encoding
-
if str.encoding == Encoding::BINARY
-
str.force_encoding(Encoding.default_external)
-
end
-
-
str
-
end
-
1
CHARSET_DETECT[:html] = method(:detect_html)
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/base'
-
1
require 'sprockets/cache/memory_store'
-
1
require 'sprockets/cached_environment'
-
-
1
module Sprockets
-
1
class Environment < Base
-
# `Environment` should be initialized with your application's root
-
# directory. This should be the same as your Rails or Rack root.
-
#
-
# env = Environment.new(Rails.root)
-
#
-
1
def initialize(root = ".")
-
1
initialize_configuration(Sprockets)
-
1
self.root = root
-
1
self.cache = Cache::MemoryStore.new
-
1
yield self if block_given?
-
end
-
-
# Returns a cached version of the environment.
-
#
-
# All of its file system calls are cached which makes `cached` much
-
# faster. This behavior is ideal in production since the file
-
# system only changes between deploys.
-
1
def cached
-
2
CachedEnvironment.new(self)
-
end
-
1
alias_method :index, :cached
-
-
1
def find_asset(*args, **options)
-
2
cached.find_asset(*args, **options)
-
end
-
-
1
def find_asset!(*args)
-
cached.find_asset!(*args)
-
end
-
-
1
def find_all_linked_assets(*args, &block)
-
cached.find_all_linked_assets(*args, &block)
-
end
-
-
1
def load(*args)
-
cached.load(*args)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'erb'
-
-
1
class Sprockets::ERBProcessor
-
# Public: Return singleton instance with default options.
-
#
-
# Returns ERBProcessor object.
-
1
def self.instance
-
@instance ||= new
-
end
-
-
1
def self.call(input)
-
instance.call(input)
-
end
-
-
1
def initialize(&block)
-
@block = block
-
end
-
-
1
def call(input)
-
match = ERB.version.match(/\Aerb\.rb \[(?<version>[^ ]+) /)
-
if match && match[:version] >= "2.2.0" # Ruby 2.6+
-
engine = ::ERB.new(input[:data], trim_mode: '<>')
-
else
-
engine = ::ERB.new(input[:data], nil, '<>')
-
end
-
engine.filename = input[:filename]
-
-
context = input[:environment].context_class.new(input)
-
klass = (class << context; self; end)
-
klass.const_set(:ENV, context.env_proxy)
-
klass.class_eval(&@block) if @block
-
-
data = engine.result(context.instance_eval('binding'))
-
context.metadata.merge(data: data)
-
end
-
end
-
# frozen_string_literal: true
-
# Define some basic Sprockets error classes
-
1
module Sprockets
-
1
class Error < StandardError; end
-
1
class ArgumentError < Error; end
-
1
class ContentTypeMismatch < Error; end
-
1
class NotImplementedError < Error; end
-
1
class NotFound < Error; end
-
1
class ConversionError < NotFound; end
-
1
class FileNotFound < NotFound; end
-
1
class FileOutsidePaths < NotFound; end
-
end
-
1
module Sprockets
-
1
module Exporters
-
# Convienence class for all exporters to inherit from
-
#
-
# An exporter is responsible for exporting a Sprockets::Asset
-
# to a file system. For example the Exporters::File class
-
# writes the asset to it's destination. The Exporters::Zlib class
-
# writes a gzip copy of the asset to disk.
-
1
class Base
-
1
attr_reader :asset, :environment, :directory, :target
-
-
# Public: Creates new instance
-
#
-
# Initialize will be called with
-
# keyword arguments:
-
#
-
# - asset: An instance of Sprockets::Asset.
-
# - environment: An instance of Sprockets::Environment.
-
# - directory: String representing the target directory to write to.
-
#
-
# These will all be stored as accessible values. In addition a
-
# +target+ will be available which is the target directory and
-
# the asset's digest path combined.
-
1
def initialize(asset: nil, environment: nil, directory: nil)
-
@asset = asset
-
@environment = environment
-
@directory = directory
-
@target = ::File.join(directory, asset.digest_path)
-
setup
-
end
-
-
# Public: Callback that is executed after intialization
-
#
-
# Any setup that needs to be done can be performed in the +setup+
-
# method. It will be called immediately after initialization.
-
1
def setup
-
end
-
-
# Public: Handles logic for skipping exporter and notifying logger
-
#
-
# The `skip?` will be called before anything will be written.
-
# If `skip?` returns truthy it will not continue. This method
-
# takes a `logger` that responds to +debug+ and +info+. The `skip?`
-
# method is the only place expected to write to a logger, any other
-
# messages may produce jumbled logs.
-
1
def skip?(logger)
-
false
-
end
-
-
# Public: Contains logic for writing "exporting" asset to disk
-
#
-
# If the exporter is not skipped it then Sprockets will execute it's
-
# `call` method. This method takes no arguments and should only use
-
# elements passed in via initialize or stored in `setup`.
-
1
def call
-
raise "Must subclass and implement call"
-
end
-
-
# Public: Yields a file that can be written to with the input
-
#
-
# `filename`. Defaults to the `target`. Method
-
# is safe to use in forked or threaded environments.
-
1
def write(filename = target)
-
FileUtils.mkdir_p File.dirname(filename)
-
PathUtils.atomic_write(filename) do |f|
-
yield f
-
end
-
end
-
end
-
end
-
end
-
1
require 'sprockets/exporters/base'
-
-
1
module Sprockets
-
1
module Exporters
-
# Writes a an asset file to disk
-
1
class FileExporter < Exporters::Base
-
1
def skip?(logger)
-
if ::File.exist?(target)
-
logger.debug "Skipping #{ target }, already exists"
-
true
-
else
-
logger.info "Writing #{ target }"
-
false
-
end
-
end
-
-
1
def call
-
write(target) do |file|
-
file.write(asset.source)
-
end
-
end
-
end
-
end
-
end
-
1
require 'sprockets/exporters/base'
-
1
require 'sprockets/utils/gzip'
-
-
1
module Sprockets
-
1
module Exporters
-
# Generates a `.gz` file using the zlib algorithm built into
-
# Ruby's standard library.
-
1
class ZlibExporter < Exporters::Base
-
1
def setup
-
@gzip_target = "#{ target }.gz"
-
@gzip = Sprockets::Utils::Gzip.new(asset, archiver: Utils::Gzip::ZlibArchiver)
-
end
-
-
1
def skip?(logger)
-
return true if environment.skip_gzip?
-
return true if @gzip.cannot_compress?
-
if ::File.exist?(@gzip_target)
-
logger.debug "Skipping #{ @gzip_target }, already exists"
-
true
-
else
-
logger.info "Writing #{ @gzip_target }"
-
false
-
end
-
end
-
-
1
def call
-
write(@gzip_target) do |file|
-
@gzip.compress(file, target)
-
end
-
end
-
end
-
end
-
end
-
1
require 'sprockets/exporters/zlib_exporter'
-
-
1
module Sprockets
-
1
module Exporters
-
# Generates a `.gz` file using the zopfli algorithm from the
-
# Zopfli gem.
-
1
class ZopfliExporter < ZlibExporter
-
1
def setup
-
@gzip_target = "#{ target }.gz"
-
@gzip = Sprockets::Utils::Gzip.new(asset, archiver: Utils::Gzip::ZopfliArchiver)
-
end
-
end
-
end
-
end
-
1
module Sprockets
-
# `Exporting` is an internal mixin whose public methods are exposed on
-
# the `Environment` and `CachedEnvironment` classes.
-
1
module Exporting
-
# Exporters are ran on the assets:precompile task
-
1
def exporters
-
config[:exporters]
-
end
-
-
# Public: Registers a new Exporter `klass` for `mime_type`.
-
#
-
# If your exporter depends on one or more other exporters you can
-
# specify this via the `depend_on` keyword.
-
#
-
# register_exporter '*/*', Sprockets::Exporters::ZlibExporter
-
#
-
# This ensures that `Sprockets::Exporters::File` will always execute before
-
# `Sprockets::Exporters::Zlib`
-
1
def register_exporter(mime_types, klass = nil)
-
2
mime_types = Array(mime_types)
-
-
2
mime_types.each do |mime_type|
-
2
self.config = hash_reassoc(config, :exporters, mime_type) do |_exporters|
-
2
_exporters << klass
-
end
-
end
-
end
-
-
# Public: Remove Exporting processor `klass` for `mime_type`.
-
#
-
# environment.unregister_exporter '*/*', Sprockets::Exporters::Zlib
-
#
-
# Can be called without a mime type
-
#
-
# environment.unregister_exporter Sprockets::Exporters::Zlib
-
#
-
# Does not remove any exporters that depend on `klass`.
-
1
def unregister_exporter(mime_types, exporter = nil)
-
unless mime_types.is_a? Array
-
if mime_types.is_a? String
-
mime_types = [mime_types]
-
else # called with no mime type
-
exporter = mime_types
-
mime_types = nil
-
end
-
end
-
-
self.config = hash_reassoc(config, :exporters) do |_exporters|
-
_exporters.each do |mime_type, exporters_array|
-
next if mime_types && !mime_types.include?(mime_type)
-
if exporters_array.include? exporter
-
_exporters[mime_type] = exporters_array.dup.delete exporter
-
end
-
end
-
end
-
end
-
-
# Public: Checks if concurrent exporting is allowed
-
1
def export_concurrent
-
config[:export_concurrent]
-
end
-
-
# Public: Enable or disable the concurrently exporting files
-
#
-
# Defaults to true.
-
#
-
# environment.export_concurrent = false
-
#
-
1
def export_concurrent=(export_concurrent)
-
self.config = config.merge(export_concurrent: export_concurrent).freeze
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'set'
-
-
1
module Sprockets
-
# Internal: The first processor in the pipeline that reads the file into
-
# memory and passes it along as `input[:data]`.
-
1
class FileReader
-
1
def self.call(input)
-
4
env = input[:environment]
-
4
data = env.read_file(input[:filename], input[:content_type])
-
4
dependencies = Set.new(input[:metadata][:dependencies])
-
4
dependencies += [env.build_file_digest_uri(input[:filename])]
-
4
{ data: data, dependencies: dependencies }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Sprockets
-
# Internal: HTTP URI utilities. Many adapted from Rack::Utils. Mixed into
-
# Environment.
-
1
module HTTPUtils
-
1
extend self
-
-
# Public: Test mime type against mime range.
-
#
-
# match_mime_type?('text/html', 'text/*') => true
-
# match_mime_type?('text/plain', '*') => true
-
# match_mime_type?('text/html', 'application/json') => false
-
#
-
# Returns true if the given value is a mime match for the given mime match
-
# specification, false otherwise.
-
1
def match_mime_type?(value, matcher)
-
39
v1, v2 = value.split('/'.freeze, 2)
-
39
m1, m2 = matcher.split('/'.freeze, 2)
-
39
(m1 == '*'.freeze || v1 == m1) && (m2.nil? || m2 == '*'.freeze || m2 == v2)
-
end
-
-
# Public: Return values from Hash where the key matches the mime type.
-
#
-
# hash - Hash of String matcher keys to Object values
-
# mime_type - String mime type
-
#
-
# Returns Array of Object values.
-
1
def match_mime_type_keys(hash, mime_type)
-
2
type, subtype = mime_type.split('/', 2)
-
[
-
2
hash["*"],
-
hash["*/*"],
-
hash["#{type}/*"],
-
hash["#{type}/#{subtype}"]
-
].compact
-
end
-
-
# Internal: Parse Accept header quality values.
-
#
-
# values - String e.g. "application/javascript"
-
#
-
# Adapted from Rack::Utils#q_values. Quality values are
-
# described in http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
-
#
-
# parse_q_values("application/javascript")
-
# # => [["application/javascript", 1.0]]
-
#
-
# parse_q_values("*/*")
-
# # => [["*/*", 1.0]]
-
#
-
# parse_q_values("text/plain; q=0.5, image/*")
-
# # => [["text/plain", 0.5], ["image/*", 1.0]]
-
#
-
# parse_q_values("application/javascript, text/css")
-
# # => [["application/javascript", 1.0], ["text/css", 1.0]]
-
#
-
# Returns an Array of [String, Float].
-
1
def parse_q_values(values)
-
4
values.to_s.split(/\s*,\s*/).map do |part|
-
4
value, parameters = part.split(/\s*;\s*/, 2)
-
4
quality = 1.0
-
4
if md = /\Aq=([\d.]+)/.match(parameters)
-
quality = md[1].to_f
-
end
-
4
[value, quality]
-
end
-
end
-
-
# Internal: Find all qvalue matches from an Array of available options.
-
#
-
# Adapted from Rack::Utils#q_values.
-
#
-
# Returns Array of matched Strings from available Array or [].
-
1
def find_q_matches(q_values, available, &matcher)
-
10
matcher ||= lambda { |a, b| a == b }
-
-
10
matches = []
-
-
10
case q_values
-
when Array
-
when String
-
3
q_values = parse_q_values(q_values)
-
when NilClass
-
q_values = []
-
else
-
raise TypeError, "unknown q_values type: #{q_values.class}"
-
end
-
-
10
i = 0
-
10
q_values.each do |accepted, quality|
-
77
if match = available.find { |option| matcher.call(option, accepted) }
-
9
i += 1
-
9
matches << [-quality, i, match]
-
end
-
end
-
-
10
matches.sort!
-
19
matches.map! { |_, _, match| match }
-
10
matches
-
end
-
-
# Internal: Find the best qvalue match from an Array of available options.
-
#
-
# Adapted from Rack::Utils#q_values.
-
#
-
# Returns the matched String from available Array or nil.
-
1
def find_best_q_match(q_values, available, &matcher)
-
10
find_q_matches(q_values, available, &matcher).first
-
end
-
-
# Internal: Find the all qvalue match from an Array of available mime type
-
# options.
-
#
-
# Adapted from Rack::Utils#q_values.
-
#
-
# Returns Array of matched mime type Strings from available Array or [].
-
1
def find_mime_type_matches(q_value_header, available)
-
find_q_matches(q_value_header, available) do |a, b|
-
match_mime_type?(a, b)
-
end
-
end
-
-
# Internal: Find the best qvalue match from an Array of available mime type
-
# options.
-
#
-
# Adapted from Rack::Utils#q_values.
-
#
-
# Returns the matched mime type String from available Array or nil.
-
1
def find_best_mime_type_match(q_value_header, available)
-
6
find_best_q_match(q_value_header, available) do |a, b|
-
7
match_mime_type?(a, b)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/autoload'
-
1
require 'sprockets/digest_utils'
-
-
1
module Sprockets
-
1
class JSMincCompressor
-
1
VERSION = '1'
-
-
1
def self.instance
-
@instance ||= new
-
end
-
-
1
def self.call(input)
-
instance.call(input)
-
end
-
-
1
def self.cache_key
-
instance.cache_key
-
end
-
-
1
attr_reader :cache_key
-
-
1
def initialize(options = {})
-
@compressor_class = Autoload::JSMinC
-
@cache_key = "#{self.class.name}:#{Autoload::JSMinC::VERSION}:#{VERSION}:#{DigestUtils.digest(options)}".freeze
-
end
-
-
1
def call(input)
-
@compressor_class.minify(input[:data])
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Sprockets
-
# Public: JST transformer.
-
#
-
# Exports server side compiled templates to an object.
-
#
-
# Name your template "users/show.ejs", "users/new.eco", etc.
-
#
-
# To accept the default options
-
#
-
# environment.register_transformer
-
# 'application/javascript+function',
-
# 'application/javascript', JstProcessor
-
#
-
# Change the default namespace.
-
#
-
# environment.register_transformer
-
# 'application/javascript+function',
-
# 'application/javascript', JstProcessor.new(namespace: 'App.templates')
-
#
-
1
class JstProcessor
-
1
def self.default_namespace
-
'this.JST'
-
end
-
-
# Public: Return singleton instance with default options.
-
#
-
# Returns JstProcessor object.
-
1
def self.instance
-
@instance ||= new
-
end
-
-
1
def self.call(input)
-
instance.call(input)
-
end
-
-
1
def initialize(namespace: self.class.default_namespace)
-
@namespace = namespace
-
end
-
-
1
def call(input)
-
data = input[:data].gsub(/$(.)/m, "\\1 ").strip
-
key = input[:name]
-
<<-JST
-
(function() { #{@namespace} || (#{@namespace} = {}); #{@namespace}[#{key.inspect}] = #{data};
-
}).call(this);
-
JST
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/asset'
-
1
require 'sprockets/digest_utils'
-
1
require 'sprockets/errors'
-
1
require 'sprockets/file_reader'
-
1
require 'sprockets/mime'
-
1
require 'sprockets/path_utils'
-
1
require 'sprockets/processing'
-
1
require 'sprockets/processor_utils'
-
1
require 'sprockets/resolve'
-
1
require 'sprockets/transformers'
-
1
require 'sprockets/uri_utils'
-
1
require 'sprockets/unloaded_asset'
-
-
1
module Sprockets
-
-
# The loader phase takes a asset URI location and returns a constructed Asset
-
# object.
-
1
module Loader
-
1
include DigestUtils, PathUtils, ProcessorUtils, URIUtils
-
1
include Mime, Processing, Resolve, Transformers
-
-
-
# Public: Load Asset by Asset URI.
-
#
-
# uri - A String containing complete URI to a file including schema
-
# and full path such as:
-
# "file:///Path/app/assets/js/app.js?type=application/javascript"
-
#
-
# Returns Asset.
-
1
def load(uri)
-
6
unloaded = UnloadedAsset.new(uri, self)
-
6
if unloaded.params.key?(:id)
-
unless asset = asset_from_cache(unloaded.asset_key)
-
id = unloaded.params.delete(:id)
-
uri_without_id = build_asset_uri(unloaded.filename, unloaded.params)
-
asset = load_from_unloaded(UnloadedAsset.new(uri_without_id, self))
-
if asset[:id] != id
-
@logger.warn "Sprockets load error: Tried to find #{uri}, but latest was id #{asset[:id]}"
-
end
-
end
-
else
-
6
asset = fetch_asset_from_dependency_cache(unloaded) do |paths|
-
# When asset is previously generated, its "dependencies" are stored in the cache.
-
# The presence of `paths` indicates dependencies were stored.
-
# We can check to see if the dependencies have not changed by "resolving" them and
-
# generating a digest key from the resolved entries. If this digest key has not
-
# changed, the asset will be pulled from cache.
-
#
-
# If this `paths` is present but the cache returns nothing then `fetch_asset_from_dependency_cache`
-
# will confusingly be called again with `paths` set to nil where the asset will be
-
# loaded from disk.
-
6
if paths
-
digest = DigestUtils.digest(resolve_dependencies(paths))
-
if uri_from_cache = cache.get(unloaded.digest_key(digest), true)
-
asset_from_cache(UnloadedAsset.new(uri_from_cache, self).asset_key)
-
end
-
else
-
6
load_from_unloaded(unloaded)
-
end
-
end
-
end
-
6
Asset.new(asset)
-
end
-
-
1
private
-
1
def compress_key_from_hash(hash, key)
-
43
return unless hash.key?(key)
-
31
value = hash[key].dup
-
31
return if !value
-
-
31
if block_given?
-
6
value.map! do |x|
-
53
if yield x
-
32
compress_from_root(x)
-
else
-
21
x
-
end
-
end
-
else
-
31
value.map! { |x| compress_from_root(x) }
-
end
-
31
hash[key] = value
-
end
-
-
-
1
def expand_key_from_hash(hash, key)
-
return unless hash.key?(key)
-
value = hash[key].dup
-
return if !value
-
if block_given?
-
value.map! do |x|
-
if yield x
-
expand_from_root(x)
-
else
-
x
-
end
-
end
-
else
-
value.map! { |x| expand_from_root(x) }
-
end
-
hash[key] = value
-
end
-
-
# Internal: Load asset hash from cache
-
#
-
# key - A String containing lookup information for an asset
-
#
-
# This method converts all "compressed" paths to absolute paths.
-
# Returns a hash of values representing an asset
-
1
def asset_from_cache(key)
-
asset = cache.get(key, true)
-
if asset
-
asset[:uri] = expand_from_root(asset[:uri])
-
asset[:load_path] = expand_from_root(asset[:load_path])
-
asset[:filename] = expand_from_root(asset[:filename])
-
expand_key_from_hash(asset[:metadata], :included)
-
expand_key_from_hash(asset[:metadata], :links)
-
expand_key_from_hash(asset[:metadata], :stubbed)
-
expand_key_from_hash(asset[:metadata], :required)
-
expand_key_from_hash(asset[:metadata], :to_load)
-
expand_key_from_hash(asset[:metadata], :to_link)
-
expand_key_from_hash(asset[:metadata], :dependencies) { |uri| uri.start_with?("file-digest://") }
-
-
asset[:metadata].each_key do |k|
-
next unless k.match?(/_dependencies\z/) # rubocop:disable Performance/EndWith
-
expand_key_from_hash(asset[:metadata], k)
-
end
-
end
-
asset
-
end
-
-
# Internal: Loads an asset and saves it to cache
-
#
-
# unloaded - An UnloadedAsset
-
#
-
# This method is only called when the given unloaded asset could not be
-
# successfully pulled from cache.
-
1
def load_from_unloaded(unloaded)
-
6
unless file?(unloaded.filename)
-
raise FileNotFound, "could not find file: #{unloaded.filename}"
-
end
-
-
path_to_split =
-
6
if index_alias = unloaded.params[:index_alias]
-
expand_from_root index_alias
-
else
-
6
unloaded.filename
-
end
-
-
6
load_path, logical_path = paths_split(config[:paths], path_to_split)
-
-
6
unless load_path
-
target = path_to_split
-
target += " (index alias of #{unloaded.filename})" if unloaded.params[:index_alias]
-
raise FileOutsidePaths, "#{target} is no longer under a load path: #{self.paths.join(', ')}"
-
end
-
-
6
extname, file_type = match_path_extname(logical_path, mime_exts)
-
6
logical_path = logical_path.chomp(extname)
-
6
name = logical_path
-
-
6
if pipeline = unloaded.params[:pipeline]
-
4
logical_path += ".#{pipeline}"
-
end
-
-
6
if type = unloaded.params[:type]
-
6
logical_path += config[:mime_types][type][:extensions].first
-
end
-
-
6
if type != file_type && !config[:transformers][file_type][type]
-
raise ConversionError, "could not convert #{file_type.inspect} to #{type.inspect}"
-
end
-
-
6
processors = processors_for(type, file_type, pipeline)
-
-
6
processors_dep_uri = build_processors_uri(type, file_type, pipeline)
-
6
dependencies = config[:dependencies] + [processors_dep_uri]
-
-
# Read into memory and process if theres a processor pipeline
-
6
if processors.any?
-
6
result = call_processors(processors, {
-
environment: self,
-
cache: self.cache,
-
uri: unloaded.uri,
-
filename: unloaded.filename,
-
load_path: load_path,
-
name: name,
-
content_type: type,
-
metadata: {
-
dependencies: dependencies
-
}
-
})
-
6
validate_processor_result!(result)
-
6
source = result.delete(:data)
-
6
metadata = result
-
6
metadata[:charset] = source.encoding.name.downcase unless metadata.key?(:charset)
-
6
metadata[:digest] = digest(source)
-
6
metadata[:length] = source.bytesize
-
6
metadata[:environment_version] = version
-
else
-
dependencies << build_file_digest_uri(unloaded.filename)
-
metadata = {
-
digest: file_digest(unloaded.filename),
-
length: self.stat(unloaded.filename).size,
-
dependencies: dependencies,
-
environment_version: version,
-
}
-
end
-
-
asset = {
-
6
uri: unloaded.uri,
-
load_path: load_path,
-
filename: unloaded.filename,
-
name: name,
-
logical_path: logical_path,
-
content_type: type,
-
source: source,
-
metadata: metadata,
-
dependencies_digest: DigestUtils.digest(resolve_dependencies(metadata[:dependencies]))
-
}
-
-
6
asset[:id] = hexdigest(asset)
-
6
asset[:uri] = build_asset_uri(unloaded.filename, unloaded.params.merge(id: asset[:id]))
-
-
6
store_asset(asset, unloaded)
-
6
asset
-
end
-
-
# Internal: Save a given asset to the cache
-
#
-
# asset - A hash containing values of loaded asset
-
# unloaded - The UnloadedAsset used to lookup the `asset`
-
#
-
# This method converts all absolute paths to "compressed" paths
-
# which are relative if they're in the root.
-
1
def store_asset(asset, unloaded)
-
# Save the asset in the cache under the new URI
-
6
cached_asset = asset.dup
-
6
cached_asset[:uri] = compress_from_root(asset[:uri])
-
6
cached_asset[:filename] = compress_from_root(asset[:filename])
-
6
cached_asset[:load_path] = compress_from_root(asset[:load_path])
-
-
6
if cached_asset[:metadata]
-
# Deep dup to avoid modifying `asset`
-
6
cached_asset[:metadata] = cached_asset[:metadata].dup
-
6
compress_key_from_hash(cached_asset[:metadata], :included)
-
6
compress_key_from_hash(cached_asset[:metadata], :links)
-
6
compress_key_from_hash(cached_asset[:metadata], :stubbed)
-
6
compress_key_from_hash(cached_asset[:metadata], :required)
-
6
compress_key_from_hash(cached_asset[:metadata], :to_load)
-
6
compress_key_from_hash(cached_asset[:metadata], :to_link)
-
59
compress_key_from_hash(cached_asset[:metadata], :dependencies) { |uri| uri.start_with?("file-digest://") }
-
-
6
cached_asset[:metadata].each do |key, value|
-
63
next unless key.match?(/_dependencies\z/) # rubocop:disable Performance/EndWith
-
1
compress_key_from_hash(cached_asset[:metadata], key)
-
end
-
end
-
-
# Unloaded asset and stored_asset now have a different URI
-
6
stored_asset = UnloadedAsset.new(asset[:uri], self)
-
6
cache.set(stored_asset.asset_key, cached_asset, true)
-
-
# Save the new relative path for the digest key of the unloaded asset
-
6
cache.set(unloaded.digest_key(asset[:dependencies_digest]), stored_asset.compressed_path, true)
-
end
-
-
-
# Internal: Resolve set of dependency URIs.
-
#
-
# uris - An Array of "dependencies" for example:
-
# ["environment-version", "environment-paths", "processors:type=text/css&file_type=text/css",
-
# "file-digest:///Full/path/app/assets/stylesheets/application.css",
-
# "processors:type=text/css&file_type=text/css&pipeline=self",
-
# "file-digest:///Full/path/app/assets/stylesheets"]
-
#
-
# Returns back array of things that the given uri depends on
-
# For example the environment version, if you're using a different version of sprockets
-
# then the dependencies should be different, this is used only for generating cache key
-
# for example the "environment-version" may be resolved to "environment-1.0-3.2.0" for
-
# version "3.2.0" of sprockets.
-
#
-
# Any paths that are returned are converted to relative paths
-
#
-
# Returns array of resolved dependencies
-
1
def resolve_dependencies(uris)
-
59
uris.map { |uri| resolve_dependency(uri) }
-
end
-
-
# Internal: Retrieves an asset based on its digest
-
#
-
# unloaded - An UnloadedAsset
-
# limit - A Fixnum which sets the maximum number of versions of "histories"
-
# stored in the cache
-
#
-
# This method attempts to retrieve the last `limit` number of histories of an asset
-
# from the cache a "history" which is an array of unresolved "dependencies" that the asset needs
-
# to compile. In this case a dependency can refer to either an asset e.g. index.js
-
# may rely on jquery.js (so jquery.js is a dependency), or other factors that may affect
-
# compilation, such as the VERSION of Sprockets (i.e. the environment) and what "processors"
-
# are used.
-
#
-
# For example a history array may look something like this
-
#
-
# [["environment-version", "environment-paths", "processors:type=text/css&file_type=text/css",
-
# "file-digest:///Full/path/app/assets/stylesheets/application.css",
-
# "processors:type=text/css&file_digesttype=text/css&pipeline=self",
-
# "file-digest:///Full/path/app/assets/stylesheets"]]
-
#
-
# Where the first entry is a Set of dependencies for last generated version of that asset.
-
# Multiple versions are stored since Sprockets keeps the last `limit` number of assets
-
# generated present in the system.
-
#
-
# If a "history" of dependencies is present in the cache, each version of "history" will be
-
# yielded to the passed block which is responsible for loading the asset. If found, the existing
-
# history will be saved with the dependency that found a valid asset moved to the front.
-
#
-
# If no history is present, or if none of the histories could be resolved to a valid asset then,
-
# the block is yielded to and expected to return a valid asset.
-
# When this happens the dependencies for the returned asset are added to the "history", and older
-
# entries are removed if the "history" is above `limit`.
-
1
def fetch_asset_from_dependency_cache(unloaded, limit = 3)
-
6
key = unloaded.dependency_history_key
-
-
6
history = cache.get(key) || []
-
6
history.each_with_index do |deps, index|
-
expanded_deps = deps.map do |path|
-
path.start_with?("file-digest://") ? expand_from_root(path) : path
-
end
-
if asset = yield(expanded_deps)
-
cache.set(key, history.rotate!(index)) if index > 0
-
return asset
-
end
-
end
-
-
6
asset = yield
-
6
deps = asset[:metadata][:dependencies].dup.map! do |uri|
-
53
uri.start_with?("file-digest://") ? compress_from_root(uri) : uri
-
end
-
6
cache.set(key, history.unshift(deps).take(limit))
-
6
asset
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'json'
-
1
require 'time'
-
-
1
require 'concurrent'
-
-
1
require 'sprockets/manifest_utils'
-
-
1
module Sprockets
-
# The Manifest logs the contents of assets compiled to a single directory. It
-
# records basic attributes about the asset for fast lookup without having to
-
# compile. A pointer from each logical path indicates which fingerprinted
-
# asset is the current one.
-
#
-
# The JSON is part of the public API and should be considered stable. This
-
# should make it easy to read from other programming languages and processes
-
# that don't have sprockets loaded. See `#assets` and `#files` for more
-
# infomation about the structure.
-
1
class Manifest
-
1
include ManifestUtils
-
-
1
attr_reader :environment
-
-
# Create new Manifest associated with an `environment`. `filename` is a full
-
# path to the manifest json file. The file may or may not already exist. The
-
# dirname of the `filename` will be used to write compiled assets to.
-
# Otherwise, if the path is a directory, the filename will default a random
-
# ".sprockets-manifest-*.json" file in that directory.
-
#
-
# Manifest.new(environment, "./public/assets/manifest.json")
-
#
-
1
def initialize(*args)
-
if args.first.is_a?(Base) || args.first.nil?
-
@environment = args.shift
-
end
-
-
@directory, @filename = args[0], args[1]
-
-
# Whether the manifest file is using the old manifest-*.json naming convention
-
@legacy_manifest = false
-
-
# Expand paths
-
@directory = File.expand_path(@directory) if @directory
-
@filename = File.expand_path(@filename) if @filename
-
-
# If filename is given as the second arg
-
if @directory && File.extname(@directory) != ""
-
@directory, @filename = nil, @directory
-
end
-
-
# Default dir to the directory of the filename
-
@directory ||= File.dirname(@filename) if @filename
-
-
# If directory is given w/o filename, pick a random manifest location
-
if @directory && @filename.nil?
-
@filename = find_directory_manifest(@directory, logger)
-
end
-
-
unless @directory && @filename
-
raise ArgumentError, "manifest requires output filename"
-
end
-
-
data = {}
-
-
begin
-
if File.exist?(@filename)
-
data = json_decode(File.read(@filename))
-
end
-
rescue JSON::ParserError => e
-
logger.error "#{@filename} is invalid: #{e.class} #{e.message}"
-
end
-
-
@data = data
-
end
-
-
# Returns String path to manifest.json file.
-
1
attr_reader :filename
-
1
alias_method :path, :filename
-
-
1
attr_reader :directory
-
1
alias_method :dir, :directory
-
-
# Returns internal assets mapping. Keys are logical paths which
-
# map to the latest fingerprinted filename.
-
#
-
# Logical path (String): Fingerprint path (String)
-
#
-
# { "application.js" => "application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js",
-
# "jquery.js" => "jquery-ae0908555a245f8266f77df5a8edca2e.js" }
-
#
-
1
def assets
-
@data['assets'] ||= {}
-
end
-
-
# Returns internal file directory listing. Keys are filenames
-
# which map to an attributes array.
-
#
-
# Fingerprint path (String):
-
# logical_path: Logical path (String)
-
# mtime: ISO8601 mtime (String)
-
# digest: Base64 hex digest (String)
-
#
-
# { "application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js" =>
-
# { 'logical_path' => "application.js",
-
# 'mtime' => "2011-12-13T21:47:08-06:00",
-
# 'digest' => "2e8e9a7c6b0aafa0c9bdeec90ea30213" } }
-
#
-
1
def files
-
@data['files'] ||= {}
-
end
-
-
# Public: Find all assets matching pattern set in environment.
-
#
-
# Returns Enumerator of Assets.
-
1
def find(*args)
-
unless environment
-
raise Error, "manifest requires environment for compilation"
-
end
-
-
return to_enum(__method__, *args) unless block_given?
-
-
environment = self.environment.cached
-
promises = args.flatten.map do |path|
-
Concurrent::Promise.execute(executor: executor) do
-
environment.find_all_linked_assets(path) do |asset|
-
yield asset
-
end
-
end
-
end
-
promises.each(&:wait!)
-
-
nil
-
end
-
-
# Public: Find the source of assets by paths.
-
#
-
# Returns Enumerator of assets file content.
-
1
def find_sources(*args)
-
return to_enum(__method__, *args) unless block_given?
-
-
if environment
-
find(*args).each do |asset|
-
yield asset.source
-
end
-
else
-
args.each do |path|
-
asset = assets[path]
-
yield File.binread(File.join(dir, asset)) if asset
-
end
-
end
-
end
-
-
# Compile asset to directory. The asset is written to a
-
# fingerprinted filename like
-
# `application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js`. An entry is
-
# also inserted into the manifest file.
-
#
-
# compile("application.js")
-
#
-
1
def compile(*args)
-
unless environment
-
raise Error, "manifest requires environment for compilation"
-
end
-
-
filenames = []
-
concurrent_exporters = []
-
-
assets_to_export = Concurrent::Array.new
-
find(*args) do |asset|
-
assets_to_export << asset
-
end
-
-
assets_to_export.each do |asset|
-
mtime = Time.now.iso8601
-
files[asset.digest_path] = {
-
'logical_path' => asset.logical_path,
-
'mtime' => mtime,
-
'size' => asset.bytesize,
-
'digest' => asset.hexdigest,
-
-
# Deprecated: Remove beta integrity attribute in next release.
-
# Callers should DigestUtils.hexdigest_integrity_uri to compute the
-
# digest themselves.
-
'integrity' => DigestUtils.hexdigest_integrity_uri(asset.hexdigest)
-
}
-
assets[asset.logical_path] = asset.digest_path
-
-
filenames << asset.filename
-
-
promise = nil
-
exporters_for_asset(asset) do |exporter|
-
next if exporter.skip?(logger)
-
-
if promise.nil?
-
promise = Concurrent::Promise.new(executor: executor) { exporter.call }
-
concurrent_exporters << promise.execute
-
else
-
concurrent_exporters << promise.then { exporter.call }
-
end
-
end
-
end
-
-
# make sure all exporters have finished before returning the main thread
-
concurrent_exporters.each(&:wait!)
-
save
-
-
filenames
-
end
-
-
# Removes file from directory and from manifest. `filename` must
-
# be the name with any directory path.
-
#
-
# manifest.remove("application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js")
-
#
-
1
def remove(filename)
-
path = File.join(dir, filename)
-
gzip = "#{path}.gz"
-
logical_path = files[filename]['logical_path']
-
-
if assets[logical_path] == filename
-
assets.delete(logical_path)
-
end
-
-
files.delete(filename)
-
FileUtils.rm(path) if File.exist?(path)
-
FileUtils.rm(gzip) if File.exist?(gzip)
-
-
save
-
-
logger.info "Removed #{filename}"
-
-
nil
-
end
-
-
# Cleanup old assets in the compile directory. By default it will
-
# keep the latest version, 2 backups and any created within the past hour.
-
#
-
# Examples
-
#
-
# To force only 1 backup to be kept, set count=1 and age=0.
-
#
-
# To only keep files created within the last 10 minutes, set count=0 and
-
# age=600.
-
#
-
1
def clean(count = 2, age = 3600)
-
asset_versions = files.group_by { |_, attrs| attrs['logical_path'] }
-
-
asset_versions.each do |logical_path, versions|
-
current = assets[logical_path]
-
-
versions.reject { |path, _|
-
path == current
-
}.sort_by { |_, attrs|
-
# Sort by timestamp
-
Time.parse(attrs['mtime'])
-
}.reverse.each_with_index.drop_while { |(_, attrs), index|
-
_age = [0, Time.now - Time.parse(attrs['mtime'])].max
-
# Keep if under age or within the count limit
-
_age < age || index < count
-
}.each { |(path, _), _|
-
# Remove old assets
-
remove(path)
-
}
-
end
-
end
-
-
# Wipe directive
-
1
def clobber
-
FileUtils.rm_r(directory) if File.exist?(directory)
-
logger.info "Removed #{directory}"
-
# if we have an environment clear the cache too
-
environment.cache.clear if environment
-
nil
-
end
-
-
# Persist manfiest back to FS
-
1
def save
-
data = json_encode(@data)
-
FileUtils.mkdir_p File.dirname(@filename)
-
PathUtils.atomic_write(@filename) do |f|
-
f.write(data)
-
end
-
end
-
-
1
private
-
-
# Given an asset, finds all exporters that
-
# match its mime-type.
-
#
-
# Will yield each expoter to the passed in block.
-
#
-
# array = []
-
# puts asset.content_type # => "application/javascript"
-
# exporters_for_asset(asset) do |exporter|
-
# array << exporter
-
# end
-
# # puts array => [Exporters::FileExporter, Exporters::ZlibExporter]
-
1
def exporters_for_asset(asset)
-
exporters = [Exporters::FileExporter]
-
-
environment.exporters.each do |mime_type, exporter_list|
-
next unless asset.content_type
-
next unless environment.match_mime_type? asset.content_type, mime_type
-
exporter_list.each do |exporter|
-
exporters << exporter
-
end
-
end
-
-
exporters.uniq!
-
-
exporters.each do |exporter|
-
yield exporter.new(asset: asset, environment: environment, directory: dir)
-
end
-
end
-
-
1
def json_decode(obj)
-
JSON.parse(obj, create_additions: false)
-
end
-
-
1
def json_encode(obj)
-
JSON.generate(obj)
-
end
-
-
1
def logger
-
if environment
-
environment.logger
-
else
-
logger = Logger.new($stderr)
-
logger.level = Logger::FATAL
-
logger
-
end
-
end
-
-
1
def executor
-
@executor ||= environment.export_concurrent ? :fast : :immediate
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'securerandom'
-
1
require 'logger'
-
-
1
module Sprockets
-
# Public: Manifest utilities.
-
1
module ManifestUtils
-
1
extend self
-
-
1
MANIFEST_RE = /^\.sprockets-manifest-[0-9a-f]{32}.json$/
-
-
# Public: Generate a new random manifest path.
-
#
-
# Manifests are not intended to be accessed publicly, but typically live
-
# alongside public assets for convenience. To avoid being served, the
-
# filename is prefixed with a "." which is usually hidden by web servers
-
# like Apache. To help in other environments that may not control this,
-
# a random hex string is appended to the filename to prevent people from
-
# guessing the location. If directory indexes are enabled on the server,
-
# all bets are off.
-
#
-
# Return String path.
-
1
def generate_manifest_path
-
".sprockets-manifest-#{SecureRandom.hex(16)}.json"
-
end
-
-
# Public: Find or pick a new manifest filename for target build directory.
-
#
-
# dirname - String dirname
-
#
-
# Examples
-
#
-
# find_directory_manifest("/app/public/assets")
-
# # => "/app/public/assets/.sprockets-manifest-abc123.json"
-
#
-
# Returns String filename.
-
1
def find_directory_manifest(dirname, logger = Logger.new($stderr))
-
entries = File.directory?(dirname) ? Dir.entries(dirname) : []
-
manifest_entries = entries.select { |e| e =~ MANIFEST_RE }
-
if manifest_entries.length > 1
-
manifest_entries.sort!
-
logger.warn("Found multiple manifests: #{manifest_entries}. Choosing the first alphabetically: #{manifest_entries.first}")
-
end
-
entry = manifest_entries.first || generate_manifest_path
-
File.join(dirname, entry)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/encoding_utils'
-
1
require 'sprockets/http_utils'
-
1
require 'sprockets/utils'
-
-
1
module Sprockets
-
1
module Mime
-
1
include HTTPUtils, Utils
-
-
# Public: Mapping of MIME type Strings to properties Hash.
-
#
-
# key - MIME Type String
-
# value - Hash
-
# extensions - Array of extnames
-
# charset - Default Encoding or function to detect encoding
-
#
-
# Returns Hash.
-
1
def mime_types
-
12
config[:mime_types]
-
end
-
-
# Internal: Mapping of MIME extension Strings to MIME type Strings.
-
#
-
# Used for internal fast lookup purposes.
-
#
-
# Examples:
-
#
-
# mime_exts['.js'] #=> 'application/javascript'
-
#
-
# key - MIME extension String
-
# value - MIME Type String
-
#
-
# Returns Hash.
-
1
def mime_exts
-
6
config[:mime_exts]
-
end
-
-
# Public: Register a new mime type.
-
#
-
# mime_type - String MIME Type
-
# extensions - Array of String extnames
-
# charset - Proc/Method that detects the charset of a file.
-
# See EncodingUtils.
-
#
-
# Returns nothing.
-
1
def register_mime_type(mime_type, extensions: [], charset: nil)
-
56
extnames = Array(extensions)
-
-
56
charset ||= :default if mime_type.start_with?('text/')
-
56
charset = EncodingUtils::CHARSET_DETECT[charset] if charset.is_a?(Symbol)
-
-
56
self.config = hash_reassoc(config, :mime_exts) do |mime_exts|
-
56
extnames.each do |extname|
-
84
mime_exts[extname] = mime_type
-
end
-
56
mime_exts
-
end
-
-
56
self.config = hash_reassoc(config, :mime_types) do |mime_types|
-
56
type = { extensions: extnames }
-
56
type[:charset] = charset if charset
-
56
mime_types.merge(mime_type => type)
-
end
-
end
-
-
# Internal: Get detecter function for MIME type.
-
#
-
# mime_type - String MIME type
-
#
-
# Returns Proc detector or nil if none is available.
-
1
def mime_type_charset_detecter(mime_type)
-
4
if type = config[:mime_types][mime_type]
-
4
if detect = type[:charset]
-
4
return detect
-
end
-
end
-
end
-
-
# Public: Read file on disk with MIME type specific encoding.
-
#
-
# filename - String path
-
# content_type - String MIME type
-
#
-
# Returns String file contents transcoded to UTF-8 or in its external
-
# encoding.
-
1
def read_file(filename, content_type = nil)
-
4
data = File.binread(filename)
-
-
4
if detect = mime_type_charset_detecter(content_type)
-
4
detect.call(data).encode(Encoding::UTF_8, universal_newline: true)
-
else
-
data
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'json'
-
-
1
module Sprockets
-
1
module Npm
-
# Internal: Override resolve_alternates to install package.json behavior.
-
#
-
# load_path - String environment path
-
# logical_path - String path relative to base
-
#
-
# Returns candiate filenames.
-
1
def resolve_alternates(load_path, logical_path)
-
4
candidates, deps = super
-
-
4
dirname = File.join(load_path, logical_path)
-
-
4
if directory?(dirname)
-
filename = File.join(dirname, 'package.json')
-
-
if self.file?(filename)
-
deps << build_file_digest_uri(filename)
-
read_package_directives(dirname, filename) do |path|
-
if file?(path)
-
candidates << path
-
end
-
end
-
end
-
end
-
-
4
return candidates, deps
-
end
-
-
# Internal: Read package.json's main and style directives.
-
#
-
# dirname - String path to component directory.
-
# filename - String path to package.json.
-
#
-
# Returns nothing.
-
1
def read_package_directives(dirname, filename)
-
package = JSON.parse(File.read(filename), create_additions: false)
-
-
case package['main']
-
when String
-
yield File.expand_path(package['main'], dirname)
-
when nil
-
yield File.expand_path('index.js', dirname)
-
end
-
-
yield File.expand_path(package['style'], dirname) if package['style']
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'set'
-
1
require 'sprockets/path_utils'
-
1
require 'sprockets/uri_utils'
-
-
1
module Sprockets
-
# Internal: Related PathUtils helpers that also track all the file system
-
# calls they make for caching purposes. All functions return a standard
-
# return value and a Set of cache dependency URIs that can be used in the
-
# future to see if the returned value should be invalidated from cache.
-
#
-
# entries_with_dependencies("app/assets/javascripts")
-
# # => [
-
# # ["application.js", "projects.js", "users.js", ...]
-
# # #<Set: {"file-digest:/path/to/app/assets/javascripts"}>
-
# # ]
-
#
-
# The returned dependency set can be passed to resolve_dependencies(deps)
-
# to check if the returned result is still fresh. In this case, entry always
-
# returns a single path, but multiple calls should accumulate dependencies
-
# into a single set thats saved off and checked later.
-
#
-
# resolve_dependencies(deps)
-
# # => "\x01\x02\x03"
-
#
-
# Later, resolving the same set again will produce a different hash if
-
# something on the file system has changed.
-
#
-
# resolve_dependencies(deps)
-
# # => "\x03\x04\x05"
-
#
-
1
module PathDependencyUtils
-
1
include PathUtils
-
1
include URIUtils
-
-
# Internal: List directory entries and return a set of dependencies that
-
# would invalid the cached return result.
-
#
-
# See PathUtils#entries
-
#
-
# path - String directory path
-
#
-
# Returns an Array of entry names and a Set of dependency URIs.
-
1
def entries_with_dependencies(path)
-
return entries(path), Set.new([build_file_digest_uri(path)])
-
end
-
-
# Internal: List directory filenames and associated Stats under a
-
# directory.
-
#
-
# See PathUtils#stat_directory
-
#
-
# dir - A String directory
-
#
-
# Returns an Array of filenames and a Set of dependency URIs.
-
1
def stat_directory_with_dependencies(dir)
-
return stat_directory(dir).to_a, Set.new([build_file_digest_uri(dir)])
-
end
-
-
# Internal: List directory filenames and associated Stats under an entire
-
# directory tree.
-
#
-
# See PathUtils#stat_sorted_tree
-
#
-
# dir - A String directory
-
#
-
# Returns an Array of filenames and a Set of dependency URIs.
-
1
def stat_sorted_tree_with_dependencies(dir)
-
1
deps = Set.new([build_file_digest_uri(dir)])
-
1
results = stat_sorted_tree(dir).map do |path, stat|
-
2
deps << build_file_digest_uri(path) if stat.directory?
-
2
[path, stat]
-
end
-
1
return results, deps
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/digest_utils'
-
1
require 'sprockets/path_utils'
-
-
1
module Sprockets
-
# Internal: Crossover of path and digest utilities functions.
-
1
module PathDigestUtils
-
1
include DigestUtils, PathUtils
-
-
# Internal: Compute digest for file stat.
-
#
-
# path - String filename
-
# stat - File::Stat
-
#
-
# Returns String digest bytes.
-
1
def stat_digest(path, stat)
-
14
if stat.directory?
-
# If its a directive, digest the list of filenames
-
2
digest_class.digest(self.entries(path).join(','.freeze))
-
12
elsif stat.file?
-
# If its a file, digest the contents
-
12
digest_class.file(path.to_s).digest
-
else
-
raise TypeError, "stat was not a directory or file: #{stat.ftype}"
-
end
-
end
-
-
# Internal: Compute digest for path.
-
#
-
# path - String filename or directory path.
-
#
-
# Returns String digest bytes or nil.
-
1
def file_digest(path)
-
if stat = self.stat(path)
-
self.stat_digest(path, stat)
-
end
-
end
-
-
# Internal: Compute digest for a set of paths.
-
#
-
# paths - Array of filename or directory paths.
-
#
-
# Returns String digest bytes.
-
1
def files_digest(paths)
-
self.digest(paths.map { |path| self.file_digest(path) })
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Sprockets
-
# Internal: File and path related utilities. Mixed into Environment.
-
#
-
# Probably would be called FileUtils, but that causes namespace annoyances
-
# when code actually wants to reference ::FileUtils.
-
1
module PathUtils
-
1
extend self
-
1
require 'pathname'
-
-
# Public: Like `File.stat`.
-
#
-
# path - String file or directory path
-
#
-
# Returns nil if the file does not exist.
-
1
def stat(path)
-
29
if File.exist?(path)
-
15
File.stat(path.to_s)
-
else
-
nil
-
end
-
end
-
-
# Public: Like `File.file?`.
-
#
-
# path - String file path.
-
#
-
# Returns true path exists and is a file.
-
1
def file?(path)
-
13
if stat = self.stat(path)
-
13
stat.file?
-
else
-
false
-
end
-
end
-
-
# Public: Like `File.directory?`.
-
#
-
# path - String file path.
-
#
-
# Returns true path exists and is a directory.
-
1
def directory?(path)
-
12
if stat = self.stat(path)
-
stat.directory?
-
else
-
12
false
-
end
-
end
-
-
# Public: A version of `Dir.entries` that filters out `.` files and `~`
-
# swap files.
-
#
-
# path - String directory path
-
#
-
# Returns an empty `Array` if the directory does not exist.
-
1
def entries(path)
-
3
if File.directory?(path)
-
3
entries = Dir.entries(path, encoding: Encoding.default_internal)
-
3
entries.reject! { |entry|
-
28
entry.start_with?(".".freeze) ||
-
22
(entry.start_with?("#".freeze) && entry.end_with?("#".freeze)) ||
-
entry.end_with?("~".freeze)
-
}
-
3
entries.sort!
-
3
entries
-
else
-
[]
-
end
-
end
-
-
# Public: Check if path is absolute or relative.
-
#
-
# path - String path.
-
#
-
# Returns true if path is absolute, otherwise false.
-
1
if File::ALT_SEPARATOR
-
# On Windows, ALT_SEPARATOR is \
-
# Delegate to Pathname since the logic gets complex.
-
def absolute_path?(path)
-
Pathname.new(path).absolute?
-
end
-
else
-
1
def absolute_path?(path)
-
19
path.start_with?(File::SEPARATOR)
-
end
-
end
-
-
1
if File::ALT_SEPARATOR
-
SEPARATOR_PATTERN = "#{Regexp.quote(File::SEPARATOR)}|#{Regexp.quote(File::ALT_SEPARATOR)}"
-
else
-
1
SEPARATOR_PATTERN = "#{Regexp.quote(File::SEPARATOR)}"
-
end
-
-
# Public: Check if path is explicitly relative.
-
# Starts with "./" or "../".
-
#
-
# path - String path.
-
#
-
# Returns true if path is relative, otherwise false.
-
1
def relative_path?(path)
-
4
path.match?(/^\.\.?($|#{SEPARATOR_PATTERN})/) ? true : false
-
end
-
-
# Public: Get relative path from `start` to `dest`.
-
#
-
# start - String start path (file or dir)
-
# dest - String destination path
-
#
-
# Returns relative String path from `start` to `dest`
-
1
def relative_path_from(start, dest)
-
22
start, dest = Pathname.new(start), Pathname.new(dest)
-
22
start = start.dirname unless start.directory?
-
22
dest.relative_path_from(start).to_s
-
end
-
-
# Public: Joins path to base path.
-
#
-
# base - Root path
-
# path - Extending path
-
#
-
# Example
-
#
-
# join('base/path/', '../file.js')
-
# # => 'base/file.js'
-
#
-
# Returns string path starting from base and ending at path
-
1
def join(base, path)
-
22
(Pathname.new(base) + path).to_s
-
end
-
-
# Public: Sets pipeline for path
-
#
-
# path - String path
-
# extensions - List of file extensions
-
# pipeline - Pipeline
-
#
-
# Examples
-
#
-
# set_pipeline('path/file.js.erb', config[:mime_exts], config[:pipeline_exts], :source)
-
# # => 'path/file.source.js.erb'
-
#
-
# set_pipeline('path/some.file.source.js.erb', config[:mime_exts], config[:pipeline_exts], :debug)
-
# # => 'path/some.file.debug.js.erb'
-
#
-
# Returns string path with pipeline parsed in
-
1
def set_pipeline(path, mime_exts, pipeline_exts, pipeline)
-
13
extension, _ = match_path_extname(path, mime_exts)
-
13
path.chomp!(extension)
-
13
pipeline_old, _ = match_path_extname(path, pipeline_exts)
-
13
path.chomp!(pipeline_old)
-
-
13
"#{path}.#{pipeline}#{extension}"
-
end
-
-
# Internal: Get relative path for root path and subpath.
-
#
-
# path - String path
-
# subpath - String subpath of path
-
#
-
# Returns relative String path if subpath is a subpath of path, or nil if
-
# subpath is outside of path.
-
1
def split_subpath(path, subpath)
-
157
return "" if path == subpath
-
156
path = File.join(path, ''.freeze)
-
156
if subpath.start_with?(path)
-
151
subpath[path.length..-1]
-
else
-
nil
-
end
-
end
-
-
# Internal: Detect root path and base for file in a set of paths.
-
#
-
# paths - Array of String paths
-
# filename - String path of file expected to be in one of the paths.
-
#
-
# Returns [String root, String path]
-
1
def paths_split(paths, filename)
-
20
paths.each do |path|
-
25
if subpath = split_subpath(path, filename)
-
20
return path, subpath
-
end
-
end
-
nil
-
end
-
-
# Internal: Get path's extensions.
-
#
-
# path - String
-
#
-
# Returns an Array of String extnames.
-
1
def path_extnames(path)
-
File.basename(path).scan(/\.[^.]+/)
-
end
-
-
# Internal: Match path extnames against available extensions.
-
#
-
# path - String
-
# extensions - Hash of String extnames to values
-
#
-
# Returns [String extname, Object value] or nil nothing matched.
-
1
def match_path_extname(path, extensions)
-
45
basename = File.basename(path)
-
-
45
i = basename.index('.'.freeze)
-
45
while i && i < basename.length - 1
-
28
extname = basename[i..-1]
-
28
if value = extensions[extname]
-
28
return extname, value
-
end
-
-
i = basename.index('.'.freeze, i+1)
-
end
-
-
nil
-
end
-
-
# Internal: Match paths in a directory against available extensions.
-
#
-
# path - String directory
-
# basename - String basename of target file
-
# extensions - Hash of String extnames to values
-
#
-
# Examples
-
#
-
# exts = { ".js" => "application/javascript" }
-
# find_matching_path_for_extensions("app/assets", "application", exts)
-
# # => ["app/assets/application.js", "application/javascript"]
-
#
-
# Returns an Array of [String path, Object value] matches.
-
1
def find_matching_path_for_extensions(path, basename, extensions)
-
4
matches = []
-
4
entries(path).each do |entry|
-
32
next unless File.basename(entry).start_with?(basename)
-
4
extname, value = match_path_extname(entry, extensions)
-
4
if basename == entry.chomp(extname)
-
4
filename = File.join(path, entry)
-
4
if file?(filename)
-
4
matches << [filename, value]
-
end
-
end
-
end
-
4
matches
-
end
-
-
# Internal: Returns all parents for path
-
#
-
# path - String absolute filename or directory
-
# root - String path to stop at (default: system root)
-
#
-
# Returns an Array of String paths.
-
1
def path_parents(path, root = nil)
-
root = "#{root}#{File::SEPARATOR}" if root
-
parents = []
-
-
loop do
-
parent = File.dirname(path)
-
break if parent == path
-
break if root && !path.start_with?(root)
-
parents << path = parent
-
end
-
-
parents
-
end
-
-
# Internal: Find target basename checking upwards from path.
-
#
-
# basename - String filename: ".sprocketsrc"
-
# path - String path to start search: "app/assets/javascripts/app.js"
-
# root - String path to stop at (default: system root)
-
#
-
# Returns String filename or nil.
-
1
def find_upwards(basename, path, root = nil)
-
path_parents(path, root).each do |dir|
-
filename = File.join(dir, basename)
-
return filename if file?(filename)
-
end
-
nil
-
end
-
-
# Public: Stat all the files under a directory.
-
#
-
# dir - A String directory
-
#
-
# Returns an Enumerator of [path, stat].
-
1
def stat_directory(dir)
-
2
return to_enum(__method__, dir) unless block_given?
-
-
1
self.entries(dir).each do |entry|
-
2
path = File.join(dir, entry)
-
2
if stat = self.stat(path)
-
2
yield path, stat
-
end
-
end
-
-
nil
-
end
-
-
# Public: Recursive stat all the files under a directory.
-
#
-
# dir - A String directory
-
#
-
# Returns an Enumerator of [path, stat].
-
1
def stat_tree(dir, &block)
-
return to_enum(__method__, dir) unless block_given?
-
-
self.stat_directory(dir) do |path, stat|
-
yield path, stat
-
-
if stat.directory?
-
stat_tree(path, &block)
-
end
-
end
-
-
nil
-
end
-
-
# Public: Recursive stat all the files under a directory in alphabetical
-
# order.
-
#
-
# dir - A String directory
-
#
-
# Returns an Enumerator of [path, stat].
-
1
def stat_sorted_tree(dir, &block)
-
2
return to_enum(__method__, dir) unless block_given?
-
-
1
self.stat_directory(dir).sort_by { |path, stat|
-
2
stat.directory? ? "#{path}/" : path
-
}.each do |path, stat|
-
2
yield path, stat
-
-
2
if stat.directory?
-
stat_sorted_tree(path, &block)
-
end
-
end
-
-
nil
-
end
-
-
# Public: Write to a file atomically. Useful for situations where you
-
# don't want other processes or threads to see half-written files.
-
#
-
# Utils.atomic_write('important.file') do |file|
-
# file.write('hello')
-
# end
-
#
-
# Returns nothing.
-
1
def atomic_write(filename)
-
dirname, basename = File.split(filename)
-
basename = [
-
basename,
-
Thread.current.object_id,
-
Process.pid,
-
rand(1000000)
-
].join('.'.freeze)
-
tmpname = File.join(dirname, basename)
-
-
File.open(tmpname, 'wb+') do |f|
-
yield f
-
end
-
-
File.rename(tmpname, filename)
-
ensure
-
File.delete(tmpname) if File.exist?(tmpname)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/path_utils'
-
1
require 'sprockets/utils'
-
-
1
module Sprockets
-
1
module Paths
-
1
include PathUtils, Utils
-
-
# Returns `Environment` root.
-
#
-
# All relative paths are expanded with root as its base. To be
-
# useful set this to your applications root directory. (`Rails.root`)
-
1
def root
-
124
config[:root]
-
end
-
-
# Internal: Change Environment root.
-
#
-
# Only the initializer should change the root.
-
1
def root=(path)
-
1
self.config = hash_reassoc(config, :root) do
-
1
File.expand_path(path)
-
end
-
end
-
1
private :root=
-
-
# Returns an `Array` of path `String`s.
-
#
-
# These paths will be used for asset logical path lookups.
-
1
def paths
-
3
config[:paths]
-
end
-
-
# Prepend a `path` to the `paths` list.
-
#
-
# Paths at the end of the `Array` have the least priority.
-
1
def prepend_path(path)
-
self.config = hash_reassoc(config, :paths) do |paths|
-
path = File.expand_path(path, config[:root]).freeze
-
paths.unshift(path)
-
end
-
end
-
-
# Append a `path` to the `paths` list.
-
#
-
# Paths at the beginning of the `Array` have a higher priority.
-
1
def append_path(path)
-
2
self.config = hash_reassoc(config, :paths) do |paths|
-
2
path = File.expand_path(path, config[:root]).freeze
-
2
paths.push(path)
-
end
-
end
-
-
# Clear all paths and start fresh.
-
#
-
# There is no mechanism for reordering paths, so its best to
-
# completely wipe the paths list and reappend them in the order
-
# you want.
-
1
def clear_paths
-
self.config = hash_reassoc(config, :paths) do |paths|
-
paths.clear
-
end
-
end
-
-
# Public: Iterate over every file under all load paths.
-
#
-
# Returns Enumerator if no block is given.
-
1
def each_file
-
return to_enum(__method__) unless block_given?
-
-
paths.each do |root|
-
stat_tree(root).each do |filename, stat|
-
if stat.file?
-
yield filename
-
end
-
end
-
end
-
-
nil
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Sprockets
-
1
module Preprocessors
-
# Private: Adds a default map to assets when one is not present
-
#
-
# If the input file already has a source map, it effectively returns the original
-
# result. Otherwise it maps 1 for 1 lines original to generated. This is needed
-
# Because other generators run after might depend on having a valid source map
-
# available.
-
1
class DefaultSourceMap
-
1
def call(input)
-
4
result = { data: input[:data] }
-
4
map = input[:metadata][:map]
-
4
filename = input[:filename]
-
4
load_path = input[:load_path]
-
4
lines = input[:data].lines.length
-
4
basename = File.basename(filename)
-
4
mime_exts = input[:environment].config[:mime_exts]
-
4
pipeline_exts = input[:environment].config[:pipeline_exts]
-
4
if map.nil? || map.empty?
-
3
result[:map] = {
-
"version" => 3,
-
"file" => PathUtils.split_subpath(load_path, filename),
-
"mappings" => default_mappings(lines),
-
"sources" => [PathUtils.set_pipeline(basename, mime_exts, pipeline_exts, :source)],
-
"names" => []
-
}
-
else
-
1
result[:map] = map
-
end
-
-
4
result[:map]["x_sprockets_linecount"] = lines
-
4
return result
-
end
-
-
1
private
-
-
1
def default_mappings(lines)
-
3
if (lines == 0)
-
2
""
-
1
elsif (lines == 1)
-
"AAAA"
-
else
-
1
"AAAA;" + "AACA;"*(lines - 2) + "AACA"
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/file_reader'
-
1
require 'sprockets/mime'
-
1
require 'sprockets/processor_utils'
-
1
require 'sprockets/uri_utils'
-
1
require 'sprockets/utils'
-
-
1
module Sprockets
-
# `Processing` is an internal mixin whose public methods are exposed on
-
# the `Environment` and `CachedEnvironment` classes.
-
1
module Processing
-
1
include ProcessorUtils, URIUtils, Utils
-
-
1
def pipelines
-
config[:pipelines]
-
end
-
-
# Registers a pipeline that will be called by `call_processor` method.
-
1
def register_pipeline(name, proc = nil, &block)
-
4
proc ||= block
-
-
4
self.config = hash_reassoc(config, :pipeline_exts) do |pipeline_exts|
-
4
pipeline_exts.merge(".#{name}".freeze => name.to_sym)
-
end
-
-
4
self.config = hash_reassoc(config, :pipelines) do |pipelines|
-
4
pipelines.merge(name.to_sym => proc)
-
end
-
end
-
-
# Preprocessors are ran before Postprocessors and Engine
-
# processors.
-
1
def preprocessors
-
config[:preprocessors]
-
end
-
1
alias_method :processors, :preprocessors
-
-
# Postprocessors are ran after Preprocessors and Engine processors.
-
1
def postprocessors
-
config[:postprocessors]
-
end
-
-
# Registers a new Preprocessor `klass` for `mime_type`.
-
#
-
# register_preprocessor 'text/css', Sprockets::DirectiveProcessor
-
#
-
# A block can be passed for to create a shorthand processor.
-
#
-
# register_preprocessor 'text/css' do |input|
-
# input[:data].gsub(...)
-
# end
-
#
-
1
def register_preprocessor(*args, &block)
-
8
register_config_processor(:preprocessors, *args, &block)
-
8
compute_transformers!(self.config[:registered_transformers])
-
end
-
1
alias_method :register_processor, :register_preprocessor
-
-
# Registers a new Postprocessor `klass` for `mime_type`.
-
#
-
# register_postprocessor 'application/javascript', Sprockets::DirectiveProcessor
-
#
-
# A block can be passed for to create a shorthand processor.
-
#
-
# register_postprocessor 'application/javascript' do |input|
-
# input[:data].gsub(...)
-
# end
-
#
-
1
def register_postprocessor(*args, &block)
-
register_config_processor(:postprocessors, *args, &block)
-
compute_transformers!(self.config[:registered_transformers])
-
end
-
-
# Remove Preprocessor `klass` for `mime_type`.
-
#
-
# unregister_preprocessor 'text/css', Sprockets::DirectiveProcessor
-
#
-
1
def unregister_preprocessor(*args)
-
unregister_config_processor(:preprocessors, *args)
-
compute_transformers!(self.config[:registered_transformers])
-
end
-
1
alias_method :unregister_processor, :unregister_preprocessor
-
-
# Remove Postprocessor `klass` for `mime_type`.
-
#
-
# unregister_postprocessor 'text/css', Sprockets::DirectiveProcessor
-
#
-
1
def unregister_postprocessor(*args)
-
unregister_config_processor(:postprocessors, *args)
-
compute_transformers!(self.config[:registered_transformers])
-
end
-
-
# Bundle Processors are ran on concatenated assets rather than
-
# individual files.
-
1
def bundle_processors
-
config[:bundle_processors]
-
end
-
-
# Registers a new Bundle Processor `klass` for `mime_type`.
-
#
-
# register_bundle_processor 'application/javascript', Sprockets::DirectiveProcessor
-
#
-
# A block can be passed for to create a shorthand processor.
-
#
-
# register_bundle_processor 'application/javascript' do |input|
-
# input[:data].gsub(...)
-
# end
-
#
-
1
def register_bundle_processor(*args, &block)
-
4
register_config_processor(:bundle_processors, *args, &block)
-
end
-
-
# Remove Bundle Processor `klass` for `mime_type`.
-
#
-
# unregister_bundle_processor 'application/javascript', Sprockets::DirectiveProcessor
-
#
-
1
def unregister_bundle_processor(*args)
-
unregister_config_processor(:bundle_processors, *args)
-
end
-
-
# Public: Register bundle metadata reducer function.
-
#
-
# Examples
-
#
-
# Sprockets.register_bundle_metadata_reducer 'application/javascript', :jshint_errors, [], :+
-
#
-
# Sprockets.register_bundle_metadata_reducer 'text/css', :selector_count, 0 { |total, count|
-
# total + count
-
# }
-
#
-
# mime_type - String MIME Type. Use '*/*' applies to all types.
-
# key - Symbol metadata key
-
# initial - Initial memo to pass to the reduce funciton (default: nil)
-
# block - Proc accepting the memo accumulator and current value
-
#
-
# Returns nothing.
-
1
def register_bundle_metadata_reducer(mime_type, key, *args, &block)
-
7
case args.size
-
when 0
-
reducer = block
-
when 1
-
1
if block_given?
-
initial = args[0]
-
reducer = block
-
else
-
1
initial = nil
-
1
reducer = args[0].to_proc
-
end
-
when 2
-
6
initial = args[0]
-
6
reducer = args[1].to_proc
-
else
-
raise ArgumentError, "wrong number of arguments (#{args.size} for 0..2)"
-
end
-
-
7
self.config = hash_reassoc(config, :bundle_reducers, mime_type) do |reducers|
-
7
reducers.merge(key => [initial, reducer])
-
end
-
end
-
-
1
protected
-
1
def resolve_processors_cache_key_uri(uri)
-
5
params = parse_uri_query_params(uri[11..-1])
-
5
processors = processors_for(params[:type], params[:file_type], params[:pipeline])
-
5
processors_cache_keys(processors)
-
end
-
-
1
def build_processors_uri(type, file_type, pipeline)
-
6
query = encode_uri_query_params(
-
type: type,
-
file_type: file_type,
-
pipeline: pipeline
-
)
-
6
"processors:#{query}"
-
end
-
-
1
def processors_for(type, file_type, pipeline)
-
11
pipeline ||= :default
-
11
if fn = config[:pipelines][pipeline.to_sym]
-
11
fn.call(self, type, file_type)
-
else
-
raise Error, "no pipeline: #{pipeline}"
-
end
-
end
-
-
1
def default_processors_for(type, file_type)
-
4
bundled_processors = config[:bundle_processors][type]
-
4
if bundled_processors.any?
-
4
bundled_processors
-
else
-
self_processors_for(type, file_type)
-
end
-
end
-
-
1
def self_processors_for(type, file_type)
-
7
processors = []
-
-
7
processors.concat config[:postprocessors][type]
-
7
if type != file_type && processor = config[:transformers][file_type][type]
-
2
processors << processor
-
end
-
7
processors.concat config[:preprocessors][file_type]
-
-
7
if processors.any? || mime_type_charset_detecter(type)
-
7
processors << FileReader
-
end
-
-
7
processors
-
end
-
-
1
private
-
1
def register_config_processor(type, mime_type, processor = nil, &block)
-
12
processor ||= block
-
-
12
self.config = hash_reassoc(config, type, mime_type) do |processors|
-
12
processors.unshift(processor)
-
12
processors
-
end
-
end
-
-
1
def unregister_config_processor(type, mime_type, processor)
-
self.config = hash_reassoc(config, type, mime_type) do |processors|
-
processors.delete_if { |p| p == processor || p.class == processor }
-
processors
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'set'
-
-
1
module Sprockets
-
# Functional utilities for dealing with Processor functions.
-
#
-
# A Processor is a general function that may modify or transform an asset as
-
# part of the pipeline. CoffeeScript to JavaScript conversion, Minification
-
# or Concatenation are all implemented as seperate Processor steps.
-
#
-
# Processors maybe any object that responds to call. So procs or a class that
-
# defines a self.call method.
-
#
-
# For ergonomics, processors may return a number of shorthand values.
-
# Unfortunately, this means that processors can not compose via ordinary
-
# function composition. The composition helpers here can help.
-
1
module ProcessorUtils
-
1
extend self
-
-
1
class CompositeProcessor < Struct.new(:processor_strategy, :param, :processors) # :nodoc:
-
1
SINGULAR = lambda { |param, input| ProcessorUtils.call_processor param, input }
-
2
PLURAL = lambda { |param, input| ProcessorUtils.call_processors param, input }
-
-
1
def self.create(processors)
-
438
if processors.length == 1
-
new SINGULAR, processors.first, processors
-
else
-
438
new PLURAL, processors, processors
-
end
-
end
-
-
1
def call(input)
-
1
processor_strategy.call param, input
-
end
-
-
1
def cache_key
-
1
ProcessorUtils.processors_cache_keys(processors)
-
end
-
end
-
-
# Public: Compose processors in right to left order.
-
#
-
# processors - Array of processors callables
-
#
-
# Returns a composed Proc.
-
1
def compose_processors(*processors)
-
438
CompositeProcessor.create processors
-
end
-
-
# Public: Invoke list of processors in right to left order.
-
#
-
# The right to left order processing mirrors standard function composition.
-
# Think about:
-
#
-
# bundle.call(uglify.call(coffee.call(input)))
-
#
-
# processors - Array of processor callables
-
# input - Hash of input data to pass to each processor
-
#
-
# Returns a Hash with :data and other processor metadata key/values.
-
1
def call_processors(processors, input)
-
7
data = input[:data] || ""
-
7
metadata = (input[:metadata] || {}).dup
-
-
7
processors.reverse_each do |processor|
-
19
result = call_processor(processor, input.merge(data: data, metadata: metadata))
-
19
data = result.delete(:data)
-
19
metadata.merge!(result)
-
end
-
-
7
metadata.merge(data: data)
-
end
-
-
# Public: Invoke processor.
-
#
-
# processor - Processor callables
-
# input - Hash of input data to pass to processor
-
#
-
# Returns a Hash with :data and other processor metadata key/values.
-
1
def call_processor(processor, input)
-
19
metadata = (input[:metadata] || {}).dup
-
19
metadata[:data] = input[:data]
-
-
19
case result = processor.call({data: "", metadata: {}}.merge(input))
-
when NilClass
-
metadata
-
when Hash
-
18
metadata.merge(result)
-
when String
-
1
metadata.merge(data: result)
-
else
-
raise TypeError, "invalid processor return type: #{result.class}"
-
end
-
end
-
-
# Internal: Get processor defined cached key.
-
#
-
# processor - Processor function
-
#
-
# Returns JSON serializable key or nil.
-
1
def processor_cache_key(processor)
-
16
processor.cache_key if processor.respond_to?(:cache_key)
-
end
-
-
# Internal: Get combined cache keys for set of processors.
-
#
-
# processors - Array of processor functions
-
#
-
# Returns Array of JSON serializable keys.
-
1
def processors_cache_keys(processors)
-
22
processors.map { |processor| processor_cache_key(processor) }
-
end
-
-
# Internal: Set of all "simple" value types allowed to be returned in
-
# processor metadata.
-
1
VALID_METADATA_VALUE_TYPES = Set.new([
-
String,
-
Symbol,
-
TrueClass,
-
FalseClass,
-
NilClass
-
1
] + (0.class == Integer ? [Integer] : [Bignum, Fixnum])).freeze
-
-
# Internal: Set of all nested compound metadata types that can nest values.
-
1
VALID_METADATA_COMPOUND_TYPES = Set.new([
-
Array,
-
Hash,
-
Set
-
]).freeze
-
-
# Internal: Hash of all "simple" value types allowed to be returned in
-
# processor metadata.
-
1
VALID_METADATA_VALUE_TYPES_HASH = VALID_METADATA_VALUE_TYPES.each_with_object({}) do |type, hash|
-
6
hash[type] = true
-
end.freeze
-
-
# Internal: Hash of all nested compound metadata types that can nest values.
-
1
VALID_METADATA_COMPOUND_TYPES_HASH = VALID_METADATA_COMPOUND_TYPES.each_with_object({}) do |type, hash|
-
3
hash[type] = true
-
end.freeze
-
-
# Internal: Set of all allowed metadata types.
-
1
VALID_METADATA_TYPES = (VALID_METADATA_VALUE_TYPES + VALID_METADATA_COMPOUND_TYPES).freeze
-
-
# Internal: Validate returned result of calling a processor pipeline and
-
# raise a friendly user error message.
-
#
-
# result - Metadata Hash returned from call_processors
-
#
-
# Returns result or raises a TypeError.
-
1
def validate_processor_result!(result)
-
6
if !result.instance_of?(Hash)
-
raise TypeError, "processor metadata result was expected to be a Hash, but was #{result.class}"
-
end
-
-
6
if !result[:data].instance_of?(String)
-
raise TypeError, "processor :data was expected to be a String, but as #{result[:data].class}"
-
end
-
-
6
result.each do |key, value|
-
45
if !key.instance_of?(Symbol)
-
raise TypeError, "processor metadata[#{key.inspect}] expected to be a Symbol"
-
end
-
end
-
-
6
result
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'set'
-
1
require 'sprockets/http_utils'
-
1
require 'sprockets/path_dependency_utils'
-
1
require 'sprockets/uri_utils'
-
-
1
module Sprockets
-
1
module Resolve
-
1
include HTTPUtils, PathDependencyUtils, URIUtils
-
-
# Public: Find Asset URI for given a logical path by searching the
-
# environment's load paths.
-
#
-
# resolve("application.js")
-
# # => "file:///path/to/app/javascripts/application.js?type=application/javascript"
-
#
-
# An accept content type can be given if the logical path doesn't have a
-
# format extension.
-
#
-
# resolve("application", accept: "application/javascript")
-
# # => "file:///path/to/app/javascripts/application.coffee?type=application/javascript"
-
#
-
# The String Asset URI is returned or nil if no results are found.
-
1
def resolve(path, load_paths: config[:paths], accept: nil, pipeline: nil, base_path: nil)
-
6
paths = load_paths
-
-
6
if valid_asset_uri?(path)
-
uri, deps = resolve_asset_uri(path)
-
6
elsif absolute_path?(path)
-
3
filename, type, deps = resolve_absolute_path(paths, path, accept)
-
3
elsif relative_path?(path)
-
1
filename, type, path_pipeline, deps, index_alias = resolve_relative_path(paths, path, base_path, accept)
-
else
-
2
filename, type, path_pipeline, deps, index_alias = resolve_logical_path(paths, path, accept)
-
end
-
-
6
if filename
-
6
uri = build_asset_uri(filename, type: type, pipeline: pipeline || path_pipeline, index_alias: index_alias)
-
end
-
-
6
return uri, deps
-
end
-
-
# Public: Same as resolve() but raises a FileNotFound exception instead of
-
# nil if no assets are found.
-
1
def resolve!(path, **kargs)
-
1
uri, deps = resolve(path, **kargs)
-
-
1
unless uri
-
message = +"couldn't find file '#{path}'"
-
-
if relative_path?(path) && kargs[:base_path]
-
load_path, _ = paths_split(config[:paths], kargs[:base_path])
-
message << " under '#{load_path}'"
-
end
-
-
message << " with type '#{kargs[:accept]}'" if kargs[:accept]
-
-
load_paths = kargs[:load_paths] || config[:paths]
-
message << "\nChecked in these paths: \n #{ load_paths.join("\n ") }"
-
-
raise FileNotFound, message
-
end
-
-
1
return uri, deps
-
end
-
-
1
protected
-
-
# Internal: Finds an asset given a URI
-
#
-
# uri - String. Contains file:// scheme, absolute path to
-
# file.
-
# e.g. "file:///Users/schneems/sprockets/test/fixtures/default/gallery.js?type=application/javascript"
-
#
-
# Returns Array. Contains a String uri and Set of dependencies
-
1
def resolve_asset_uri(uri)
-
filename, _ = URIUtils.parse_asset_uri(uri)
-
return uri, Set.new( [URIUtils.build_file_digest_uri(filename)] )
-
end
-
-
# Internal: Finds a file in a set of given paths
-
#
-
# paths - Array of Strings.
-
# filename - String containing absolute path to a file including extension.
-
# e.g. "/Users/schneems/sprockets/test/fixtures/asset/application.js"
-
# accept - String. A Quality value incoded set of
-
# mime types that we are looking for. Can be nil.
-
# e.g. "application/javascript" or "text/css, */*"
-
#
-
# Returns Array. Filename, type, path_pipeline, deps, index_alias
-
1
def resolve_absolute_path(paths, filename, accept)
-
3
deps = Set.new
-
3
filename = File.expand_path(filename)
-
-
# Ensure path is under load paths
-
3
return nil, nil, deps unless PathUtils.paths_split(paths, filename)
-
-
3
_, mime_type = PathUtils.match_path_extname(filename, config[:mime_exts])
-
3
type = resolve_transform_type(mime_type, accept)
-
3
return nil, nil, deps if accept && !type
-
-
3
return nil, nil, deps unless file?(filename)
-
-
3
deps << URIUtils.build_file_digest_uri(filename)
-
3
return filename, type, deps
-
end
-
-
# Internal: Finds a relative file in a set of given paths
-
#
-
# paths - Array of Strings.
-
# path - String. A relative filename with or without extension
-
# e.g. "./jquery" or "../foo.js"
-
# dirname - String. Base path where we start looking for the given file.
-
# accept - String. A Quality value incoded set of
-
# mime types that we are looking for. Can be nil.
-
# e.g. "application/javascript" or "text/css, */*"
-
#
-
# Returns Array. Filename, type, path_pipeline, deps, index_alias
-
1
def resolve_relative_path(paths, path, dirname, accept)
-
1
filename = File.expand_path(path, dirname)
-
1
load_path, _ = PathUtils.paths_split(paths, dirname)
-
1
if load_path && logical_path = PathUtils.split_subpath(load_path, filename)
-
1
resolve_logical_path([load_path], logical_path, accept)
-
else
-
return nil, nil, nil, Set.new
-
end
-
end
-
-
# Internal: Finds a file in a set of given paths
-
#
-
# paths - Array of Strings.
-
# logical_path - String. A filename with extension
-
# e.g. "coffee/foo.js" or "foo.js"
-
# accept - String. A Quality value incoded set of
-
# mime types that we are looking for. Can be nil.
-
# e.g. "application/javascript" or "text/css, */*"
-
#
-
# Finds a file on the given paths.
-
#
-
# Returns Array. Filename, type, path_pipeline, deps, index_alias
-
1
def resolve_logical_path(paths, logical_path, accept)
-
3
extname, mime_type = PathUtils.match_path_extname(logical_path, config[:mime_exts])
-
3
logical_name = logical_path.chomp(extname)
-
-
3
extname, pipeline = PathUtils.match_path_extname(logical_name, config[:pipeline_exts])
-
3
logical_name = logical_name.chomp(extname)
-
-
3
parsed_accept = parse_accept_options(mime_type, accept)
-
3
transformed_accepts = expand_transform_accepts(parsed_accept)
-
-
3
filename, mime_type, deps, index_alias = resolve_under_paths(paths, logical_name, transformed_accepts)
-
-
3
if filename
-
3
deps << build_file_digest_uri(filename)
-
3
type = resolve_transform_type(mime_type, parsed_accept)
-
3
return filename, type, pipeline, deps, index_alias
-
else
-
return nil, nil, nil, deps
-
end
-
end
-
-
# Internal: Finds a file in a set of given paths
-
#
-
# paths - Array of Strings.
-
# logical_name - String. A filename without extension
-
# e.g. "application" or "coffee/foo"
-
# accepts - Array of array containing mime/version pairs
-
# e.g. [["application/javascript", 1.0]]
-
#
-
# Finds a file with the same name as `logical_name` or "index" inside
-
# of the `logical_name` directory that matches a valid mime-type/version from
-
# `accepts`.
-
#
-
# Returns Array. Filename, type, dependencies, and index_alias
-
1
def resolve_under_paths(paths, logical_name, accepts)
-
3
deps = Set.new
-
3
return nil, nil, deps if accepts.empty?
-
-
# TODO: Allow new path resolves to be registered
-
3
@resolvers ||= [
-
method(:resolve_main_under_path),
-
method(:resolve_alts_under_path),
-
method(:resolve_index_under_path)
-
]
-
3
mime_exts = config[:mime_exts]
-
-
3
paths.each do |load_path|
-
4
candidates = []
-
4
@resolvers.each do |fn|
-
12
result = fn.call(load_path, logical_name, mime_exts)
-
12
candidates.concat(result[0])
-
12
deps.merge(result[1])
-
end
-
-
4
candidate = HTTPUtils.find_best_q_match(accepts, candidates) do |c, matcher|
-
32
match_mime_type?(c[:type] || "application/octet-stream", matcher)
-
end
-
4
return candidate[:filename], candidate[:type], deps, candidate[:index_alias] if candidate
-
end
-
-
return nil, nil, deps
-
end
-
-
# Internal: Finds candidate files on a given path
-
#
-
# load_path - String. An absolute path to a directory
-
# logical_name - String. A filename without extension
-
# e.g. "application" or "coffee/foo"
-
# mime_exts - Hash of file extensions and their mime types
-
# e.g. {".xml.builder"=>"application/xml+builder"}
-
#
-
# Finds files that match a given `logical_name` with an acceptable
-
# mime type that is included in `mime_exts` on the `load_path`.
-
#
-
# Returns Array. First element is an Array of hashes or empty, second is a String
-
1
def resolve_main_under_path(load_path, logical_name, mime_exts)
-
4
dirname = File.dirname(File.join(load_path, logical_name))
-
4
candidates = self.find_matching_path_for_extensions(dirname, File.basename(logical_name), mime_exts)
-
4
candidates.map! do |c|
-
4
{ filename: c[0], type: c[1] }
-
end
-
4
return candidates, [ URIUtils.build_file_digest_uri(dirname) ]
-
end
-
-
-
# Internal: Finds candidate index files in a given path
-
#
-
# load_path - String. An absolute path to a directory
-
# logical_name - String. A filename without extension
-
# e.g. "application" or "coffee/foo"
-
# mime_exts - Hash of file extensions and their mime types
-
# e.g. {".xml.builder"=>"application/xml+builder"}
-
#
-
# Looking in the given `load_path` this method will find all files under the `logical_name` directory
-
# that are named `index` and have a matching mime type in `mime_exts`.
-
#
-
# Returns Array. First element is an Array of hashes or empty, second is a String
-
1
def resolve_index_under_path(load_path, logical_name, mime_exts)
-
4
dirname = File.join(load_path, logical_name)
-
-
4
if self.directory?(dirname)
-
candidates = self.find_matching_path_for_extensions(dirname, "index".freeze, mime_exts)
-
else
-
4
candidates = []
-
end
-
-
4
candidates.map! do |c|
-
{ filename: c[0],
-
type: c[1],
-
index_alias: compress_from_root(c[0].sub(/\/index(\.[^\/]+)$/, '\1')) }
-
end
-
-
4
return candidates, [ URIUtils.build_file_digest_uri(dirname) ]
-
end
-
-
1
def resolve_alts_under_path(load_path, logical_name, mime_exts)
-
4
filenames, deps = self.resolve_alternates(load_path, logical_name)
-
4
filenames.map! do |fn|
-
_, mime_type = PathUtils.match_path_extname(fn, mime_exts)
-
{ filename: fn, type: mime_type }
-
end
-
4
return filenames, deps
-
end
-
-
# Internal: Converts mimetype into accept Array
-
#
-
# - mime_type - String, optional. e.g. "text/html"
-
# - explicit_type - String, optional. e.g. "application/javascript"
-
#
-
# When called with an explicit_type and a mime_type, only a mime_type
-
# that matches the given explicit_type will be accepted.
-
#
-
# Returns Array of Array
-
#
-
# [["application/javascript", 1.0]]
-
# [["*/*", 1.0]]
-
# []
-
1
def parse_accept_options(mime_type, explicit_type)
-
3
if mime_type
-
2
return [[mime_type, 1.0]] if explicit_type.nil?
-
return [[mime_type, 1.0]] if HTTPUtils.parse_q_values(explicit_type).any? { |accept, _| HTTPUtils.match_mime_type?(mime_type, accept) }
-
return []
-
end
-
-
1
accepts = HTTPUtils.parse_q_values(explicit_type)
-
1
accepts << ['*/*'.freeze, 1.0] if accepts.empty?
-
1
return accepts
-
end
-
-
1
def resolve_alternates(load_path, logical_name)
-
4
return [], Set.new
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/autoload'
-
1
require 'sprockets/digest_utils'
-
1
require 'sprockets/source_map_utils'
-
-
1
module Sprockets
-
# Public: Sass CSS minifier.
-
#
-
# To accept the default options
-
#
-
# environment.register_bundle_processor 'text/css',
-
# Sprockets::SassCompressor
-
#
-
# Or to pass options to the Sass::Engine class.
-
#
-
# environment.register_bundle_processor 'text/css',
-
# Sprockets::SassCompressor.new({ ... })
-
#
-
1
class SassCompressor
-
1
VERSION = '1'
-
-
# Public: Return singleton instance with default options.
-
#
-
# Returns SassCompressor object.
-
1
def self.instance
-
@instance ||= new
-
end
-
-
1
def self.call(input)
-
instance.call(input)
-
end
-
-
1
def self.cache_key
-
instance.cache_key
-
end
-
-
1
attr_reader :cache_key
-
-
1
def initialize(options = {})
-
@options = {
-
syntax: :scss,
-
cache: false,
-
read_cache: false,
-
style: :compressed
-
}.merge(options).freeze
-
@cache_key = "#{self.class.name}:#{Autoload::Sass::VERSION}:#{VERSION}:#{DigestUtils.digest(options)}".freeze
-
end
-
-
1
def call(input)
-
css, map = Autoload::Sass::Engine.new(
-
input[:data],
-
@options.merge(filename: input[:filename])
-
).render_with_sourcemap('')
-
-
css = css.sub("/*# sourceMappingURL= */\n", '')
-
-
map = SourceMapUtils.format_source_map(JSON.parse(map.to_json(css_uri: '')), input)
-
map = SourceMapUtils.combine_source_maps(input[:metadata][:map], map)
-
-
{ data: css, map: map }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/autoload'
-
1
require 'sprockets/source_map_utils'
-
-
1
module Sprockets
-
# Public: Sass CSS minifier.
-
#
-
# To accept the default options
-
#
-
# environment.register_bundle_processor 'text/css',
-
# Sprockets::SasscCompressor
-
#
-
# Or to pass options to the Sass::Engine class.
-
#
-
# environment.register_bundle_processor 'text/css',
-
# Sprockets::SasscCompressor.new({ ... })
-
#
-
1
class SasscCompressor
-
# Public: Return singleton instance with default options.
-
#
-
# Returns SasscCompressor object.
-
1
def self.instance
-
1
@instance ||= new
-
end
-
-
1
def self.call(input)
-
1
instance.call(input)
-
end
-
-
1
def initialize(options = {})
-
1
@options = {
-
syntax: :scss,
-
style: :compressed,
-
source_map_contents: false,
-
omit_source_map_url: true,
-
}.merge(options).freeze
-
end
-
-
1
def call(input)
-
# SassC requires the template to be modifiable
-
1
input_data = input[:data].frozen? ? input[:data].dup : input[:data]
-
1
engine = Autoload::SassC::Engine.new(input_data, @options.merge(filename: input[:filename], source_map_file: "#{input[:filename]}.map"))
-
-
1
css = engine.render.sub(/^\n^\/\*# sourceMappingURL=.*\*\/$/m, '')
-
-
begin
-
1
map = SourceMapUtils.format_source_map(JSON.parse(engine.source_map), input)
-
1
map = SourceMapUtils.combine_source_maps(input[:metadata][:map], map)
-
rescue SassC::NotRenderedError
-
map = input[:metadata][:map]
-
end
-
-
1
{ data: css, map: map }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'rack/utils'
-
1
require 'sprockets/autoload'
-
1
require 'sprockets/source_map_utils'
-
1
require 'uri'
-
-
1
module Sprockets
-
# Processor engine class for the SASS/SCSS compiler. Depends on the `sassc` gem.
-
#
-
# For more infomation see:
-
#
-
# https://github.com/sass/sassc-ruby
-
# https://github.com/sass/sassc-rails
-
#
-
1
class SasscProcessor
-
-
# Internal: Defines default sass syntax to use. Exposed so the ScsscProcessor
-
# may override it.
-
1
def self.syntax
-
:sass
-
end
-
-
# Public: Return singleton instance with default options.
-
#
-
# Returns SasscProcessor object.
-
1
def self.instance
-
2
@instance ||= new
-
end
-
-
1
def self.call(input)
-
1
instance.call(input)
-
end
-
-
1
def self.cache_key
-
1
instance.cache_key
-
end
-
-
1
attr_reader :cache_key
-
-
1
def initialize(options = {}, &block)
-
1
@cache_version = options[:cache_version]
-
1
@cache_key = "#{self.class.name}:#{VERSION}:#{Autoload::SassC::VERSION}:#{@cache_version}".freeze
-
1
@importer_class = options[:importer]
-
1
@sass_config = options[:sass_config] || {}
-
1
@functions = Module.new do
-
1
include Functions
-
1
include options[:functions] if options[:functions]
-
1
class_eval(&block) if block_given?
-
end
-
end
-
-
1
def call(input)
-
1
context = input[:environment].context_class.new(input)
-
-
1
options = engine_options(input, context)
-
1
engine = Autoload::SassC::Engine.new(input[:data], options)
-
-
1
css = Utils.module_include(Autoload::SassC::Script::Functions, @functions) do
-
1
engine.render.sub(/^\n^\/\*# sourceMappingURL=.*\*\/$/m, '')
-
end
-
-
begin
-
1
map = SourceMapUtils.format_source_map(JSON.parse(engine.source_map), input)
-
1
map = SourceMapUtils.combine_source_maps(input[:metadata][:map], map)
-
-
1
engine.dependencies.each do |dependency|
-
8
context.metadata[:dependencies] << URIUtils.build_file_digest_uri(dependency.filename)
-
end
-
rescue SassC::NotRenderedError
-
map = input[:metadata][:map]
-
end
-
-
1
context.metadata.merge(data: css, map: map)
-
end
-
-
1
private
-
-
1
def merge_options(options)
-
1
defaults = @sass_config.dup
-
-
1
if load_paths = defaults.delete(:load_paths)
-
options[:load_paths] += load_paths
-
end
-
-
1
options.merge!(defaults)
-
1
options
-
end
-
-
# Public: Functions injected into Sass context during Sprockets evaluation.
-
#
-
# This module may be extended to add global functionality to all Sprockets
-
# Sass environments. Though, scoping your functions to just your environment
-
# is preferred.
-
#
-
# module Sprockets::SasscProcessor::Functions
-
# def asset_path(path, options = {})
-
# end
-
# end
-
#
-
1
module Functions
-
# Public: Generate a url for asset path.
-
#
-
# Default implementation is deprecated. Currently defaults to
-
# Context#asset_path.
-
#
-
# Will raise NotImplementedError in the future. Users should provide their
-
# own base implementation.
-
#
-
# Returns a SassC::Script::Value::String.
-
1
def asset_path(path, options = {})
-
path = path.value
-
-
path, _, query, fragment = URI.split(path)[5..8]
-
path = sprockets_context.asset_path(path, options)
-
query = "?#{query}" if query
-
fragment = "##{fragment}" if fragment
-
-
Autoload::SassC::Script::Value::String.new("#{path}#{query}#{fragment}", :string)
-
end
-
-
# Public: Generate a asset url() link.
-
#
-
# path - SassC::Script::Value::String URL path
-
#
-
# Returns a SassC::Script::Value::String.
-
1
def asset_url(path, options = {})
-
Autoload::SassC::Script::Value::String.new("url(#{asset_path(path, options).value})")
-
end
-
-
# Public: Generate url for image path.
-
#
-
# path - SassC::Script::Value::String URL path
-
#
-
# Returns a SassC::Script::Value::String.
-
1
def image_path(path)
-
asset_path(path, type: :image)
-
end
-
-
# Public: Generate a image url() link.
-
#
-
# path - SassC::Script::Value::String URL path
-
#
-
# Returns a SassC::Script::Value::String.
-
1
def image_url(path)
-
asset_url(path, type: :image)
-
end
-
-
# Public: Generate url for video path.
-
#
-
# path - SassC::Script::Value::String URL path
-
#
-
# Returns a SassC::Script::Value::String.
-
1
def video_path(path)
-
asset_path(path, type: :video)
-
end
-
-
# Public: Generate a video url() link.
-
#
-
# path - SassC::Script::Value::String URL path
-
#
-
# Returns a SassC::Script::Value::String.
-
1
def video_url(path)
-
asset_url(path, type: :video)
-
end
-
-
# Public: Generate url for audio path.
-
#
-
# path - SassC::Script::Value::String URL path
-
#
-
# Returns a SassC::Script::Value::String.
-
1
def audio_path(path)
-
asset_path(path, type: :audio)
-
end
-
-
# Public: Generate a audio url() link.
-
#
-
# path - SassC::Script::Value::String URL path
-
#
-
# Returns a SassC::Script::Value::String.
-
1
def audio_url(path)
-
asset_url(path, type: :audio)
-
end
-
-
# Public: Generate url for font path.
-
#
-
# path - SassC::Script::Value::String URL path
-
#
-
# Returns a SassC::Script::Value::String.
-
1
def font_path(path)
-
asset_path(path, type: :font)
-
end
-
-
# Public: Generate a font url() link.
-
#
-
# path - SassC::Script::Value::String URL path
-
#
-
# Returns a SassC::Script::Value::String.
-
1
def font_url(path)
-
asset_url(path, type: :font)
-
end
-
-
# Public: Generate url for javascript path.
-
#
-
# path - SassC::Script::Value::String URL path
-
#
-
# Returns a SassC::Script::Value::String.
-
1
def javascript_path(path)
-
asset_path(path, type: :javascript)
-
end
-
-
# Public: Generate a javascript url() link.
-
#
-
# path - SassC::Script::Value::String URL path
-
#
-
# Returns a SassC::Script::Value::String.
-
1
def javascript_url(path)
-
asset_url(path, type: :javascript)
-
end
-
-
# Public: Generate url for stylesheet path.
-
#
-
# path - SassC::Script::Value::String URL path
-
#
-
# Returns a SassC::Script::Value::String.
-
1
def stylesheet_path(path)
-
asset_path(path, type: :stylesheet)
-
end
-
-
# Public: Generate a stylesheet url() link.
-
#
-
# path - SassC::Script::Value::String URL path
-
#
-
# Returns a SassC::Script::Value::String.
-
1
def stylesheet_url(path)
-
asset_url(path, type: :stylesheet)
-
end
-
-
# Public: Generate a data URI for asset path.
-
#
-
# path - SassC::Script::Value::String logical asset path
-
#
-
# Returns a SassC::Script::Value::String.
-
1
def asset_data_url(path)
-
url = sprockets_context.asset_data_uri(path.value)
-
Autoload::SassC::Script::Value::String.new("url(" + url + ")")
-
end
-
-
1
protected
-
# Public: The Environment.
-
#
-
# Returns Sprockets::Environment.
-
1
def sprockets_environment
-
options[:sprockets][:environment]
-
end
-
-
# Public: Mutatable set of dependencies.
-
#
-
# Returns a Set.
-
1
def sprockets_dependencies
-
options[:sprockets][:dependencies]
-
end
-
-
# Deprecated: Get the Context instance. Use APIs on
-
# sprockets_environment or sprockets_dependencies directly.
-
#
-
# Returns a Context instance.
-
1
def sprockets_context
-
options[:sprockets][:context]
-
end
-
-
end
-
-
1
def engine_options(input, context)
-
1
merge_options({
-
filename: input[:filename],
-
syntax: self.class.syntax,
-
load_paths: input[:environment].paths,
-
importer: @importer_class,
-
source_map_contents: false,
-
source_map_file: "#{input[:filename]}.map",
-
omit_source_map_url: true,
-
sprockets: {
-
context: context,
-
environment: input[:environment],
-
dependencies: context.metadata[:dependencies]
-
}
-
})
-
end
-
end
-
-
-
1
class ScsscProcessor < SasscProcessor
-
1
def self.syntax
-
1
:scss
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'set'
-
1
require 'time'
-
1
require 'rack/utils'
-
-
1
module Sprockets
-
# `Server` is a concern mixed into `Environment` and
-
# `CachedEnvironment` that provides a Rack compatible `call`
-
# interface and url generation helpers.
-
1
module Server
-
# Supported HTTP request methods.
-
1
ALLOWED_REQUEST_METHODS = ['GET', 'HEAD'].to_set.freeze
-
-
# `call` implements the Rack 1.x specification which accepts an
-
# `env` Hash and returns a three item tuple with the status code,
-
# headers, and body.
-
#
-
# Mapping your environment at a url prefix will serve all assets
-
# in the path.
-
#
-
# map "/assets" do
-
# run Sprockets::Environment.new
-
# end
-
#
-
# A request for `"/assets/foo/bar.js"` will search your
-
# environment for `"foo/bar.js"`.
-
1
def call(env)
-
2
start_time = Time.now.to_f
-
4
time_elapsed = lambda { ((Time.now.to_f - start_time) * 1000).to_i }
-
-
2
unless ALLOWED_REQUEST_METHODS.include? env['REQUEST_METHOD']
-
return method_not_allowed_response
-
end
-
-
2
msg = "Served asset #{env['PATH_INFO']} -"
-
-
# Extract the path from everything after the leading slash
-
2
path = Rack::Utils.unescape(env['PATH_INFO'].to_s.sub(/^\//, ''))
-
-
2
unless path.valid_encoding?
-
return bad_request_response(env)
-
end
-
-
# Strip fingerprint
-
2
if fingerprint = path_fingerprint(path)
-
path = path.sub("-#{fingerprint}", '')
-
end
-
-
# URLs containing a `".."` are rejected for security reasons.
-
2
if forbidden_request?(path)
-
return forbidden_response(env)
-
end
-
-
2
if fingerprint
-
if_match = fingerprint
-
2
elsif env['HTTP_IF_MATCH']
-
if_match = env['HTTP_IF_MATCH'][/"(\w+)"$/, 1]
-
end
-
-
2
if env['HTTP_IF_NONE_MATCH']
-
if_none_match = env['HTTP_IF_NONE_MATCH'][/"(\w+)"$/, 1]
-
end
-
-
# Look up the asset.
-
2
asset = find_asset(path)
-
-
2
if asset.nil?
-
status = :not_found
-
2
elsif fingerprint && asset.etag != fingerprint
-
status = :not_found
-
2
elsif if_match && asset.etag != if_match
-
status = :precondition_failed
-
2
elsif if_none_match && asset.etag == if_none_match
-
status = :not_modified
-
else
-
2
status = :ok
-
end
-
-
2
case status
-
when :ok
-
2
logger.info "#{msg} 200 OK (#{time_elapsed.call}ms)"
-
2
ok_response(asset, env)
-
when :not_modified
-
logger.info "#{msg} 304 Not Modified (#{time_elapsed.call}ms)"
-
not_modified_response(env, if_none_match)
-
when :not_found
-
logger.info "#{msg} 404 Not Found (#{time_elapsed.call}ms)"
-
not_found_response(env)
-
when :precondition_failed
-
logger.info "#{msg} 412 Precondition Failed (#{time_elapsed.call}ms)"
-
precondition_failed_response(env)
-
end
-
rescue Exception => e
-
logger.error "Error compiling asset #{path}:"
-
logger.error "#{e.class.name}: #{e.message}"
-
-
case File.extname(path)
-
when ".js"
-
# Re-throw JavaScript asset exceptions to the browser
-
logger.info "#{msg} 500 Internal Server Error\n\n"
-
return javascript_exception_response(e)
-
when ".css"
-
# Display CSS asset exceptions in the browser
-
logger.info "#{msg} 500 Internal Server Error\n\n"
-
return css_exception_response(e)
-
else
-
raise
-
end
-
end
-
-
1
private
-
1
def forbidden_request?(path)
-
# Prevent access to files elsewhere on the file system
-
#
-
# http://example.org/assets/../../../etc/passwd
-
#
-
2
path.include?("..") || absolute_path?(path) || path.include?("://")
-
end
-
-
1
def head_request?(env)
-
2
env['REQUEST_METHOD'] == 'HEAD'
-
end
-
-
# Returns a 200 OK response tuple
-
1
def ok_response(asset, env)
-
2
if head_request?(env)
-
[ 200, headers(env, asset, 0), [] ]
-
else
-
2
[ 200, headers(env, asset, asset.length), asset ]
-
end
-
end
-
-
# Returns a 304 Not Modified response tuple
-
1
def not_modified_response(env, etag)
-
[ 304, cache_headers(env, etag), [] ]
-
end
-
-
# Returns a 400 Forbidden response tuple
-
1
def bad_request_response(env)
-
if head_request?(env)
-
[ 400, { "Content-Type" => "text/plain", "Content-Length" => "0" }, [] ]
-
else
-
[ 400, { "Content-Type" => "text/plain", "Content-Length" => "11" }, [ "Bad Request" ] ]
-
end
-
end
-
-
# Returns a 403 Forbidden response tuple
-
1
def forbidden_response(env)
-
if head_request?(env)
-
[ 403, { "Content-Type" => "text/plain", "Content-Length" => "0" }, [] ]
-
else
-
[ 403, { "Content-Type" => "text/plain", "Content-Length" => "9" }, [ "Forbidden" ] ]
-
end
-
end
-
-
# Returns a 404 Not Found response tuple
-
1
def not_found_response(env)
-
if head_request?(env)
-
[ 404, { "Content-Type" => "text/plain", "Content-Length" => "0", "X-Cascade" => "pass" }, [] ]
-
else
-
[ 404, { "Content-Type" => "text/plain", "Content-Length" => "9", "X-Cascade" => "pass" }, [ "Not found" ] ]
-
end
-
end
-
-
1
def method_not_allowed_response
-
[ 405, { "Content-Type" => "text/plain", "Content-Length" => "18" }, [ "Method Not Allowed" ] ]
-
end
-
-
1
def precondition_failed_response(env)
-
if head_request?(env)
-
[ 412, { "Content-Type" => "text/plain", "Content-Length" => "0", "X-Cascade" => "pass" }, [] ]
-
else
-
[ 412, { "Content-Type" => "text/plain", "Content-Length" => "19", "X-Cascade" => "pass" }, [ "Precondition Failed" ] ]
-
end
-
end
-
-
# Returns a JavaScript response that re-throws a Ruby exception
-
# in the browser
-
1
def javascript_exception_response(exception)
-
err = "#{exception.class.name}: #{exception.message}\n (in #{exception.backtrace[0]})"
-
body = "throw Error(#{err.inspect})"
-
[ 200, { "Content-Type" => "application/javascript", "Content-Length" => body.bytesize.to_s }, [ body ] ]
-
end
-
-
# Returns a CSS response that hides all elements on the page and
-
# displays the exception
-
1
def css_exception_response(exception)
-
message = "\n#{exception.class.name}: #{exception.message}"
-
backtrace = "\n #{exception.backtrace.first}"
-
-
body = <<-CSS
-
html {
-
padding: 18px 36px;
-
}
-
-
head {
-
display: block;
-
}
-
-
body {
-
margin: 0;
-
padding: 0;
-
}
-
-
body > * {
-
display: none !important;
-
}
-
-
head:after, body:before, body:after {
-
display: block !important;
-
}
-
-
head:after {
-
font-family: sans-serif;
-
font-size: large;
-
font-weight: bold;
-
content: "Error compiling CSS asset";
-
}
-
-
body:before, body:after {
-
font-family: monospace;
-
white-space: pre-wrap;
-
}
-
-
body:before {
-
font-weight: bold;
-
content: "#{escape_css_content(message)}";
-
}
-
-
body:after {
-
content: "#{escape_css_content(backtrace)}";
-
}
-
CSS
-
-
[ 200, { "Content-Type" => "text/css; charset=utf-8", "Content-Length" => body.bytesize.to_s }, [ body ] ]
-
end
-
-
# Escape special characters for use inside a CSS content("...") string
-
1
def escape_css_content(content)
-
content.
-
gsub('\\', '\\\\005c ').
-
gsub("\n", '\\\\000a ').
-
gsub('"', '\\\\0022 ').
-
gsub('/', '\\\\002f ')
-
end
-
-
1
def cache_headers(env, etag)
-
2
headers = {}
-
-
# Set caching headers
-
2
headers["Cache-Control"] = +"public"
-
2
headers["ETag"] = %("#{etag}")
-
-
# If the request url contains a fingerprint, set a long
-
# expires on the response
-
2
if path_fingerprint(env["PATH_INFO"])
-
headers["Cache-Control"] << ", max-age=31536000, immutable"
-
-
# Otherwise set `must-revalidate` since the asset could be modified.
-
else
-
2
headers["Cache-Control"] << ", must-revalidate"
-
2
headers["Vary"] = "Accept-Encoding"
-
end
-
-
2
headers
-
end
-
-
1
def headers(env, asset, length)
-
2
headers = {}
-
-
# Set content length header
-
2
headers["Content-Length"] = length.to_s
-
-
# Set content type header
-
2
if type = asset.content_type
-
# Set charset param for text/* mime types
-
2
if type.start_with?("text/") && asset.charset
-
1
type += "; charset=#{asset.charset}"
-
end
-
2
headers["Content-Type"] = type
-
end
-
-
2
headers.merge(cache_headers(env, asset.etag))
-
end
-
-
# Gets ETag fingerprint.
-
#
-
# "foo-0aa2105d29558f3eb790d411d7d8fb66.js"
-
# # => "0aa2105d29558f3eb790d411d7d8fb66"
-
#
-
1
def path_fingerprint(path)
-
4
path[/-([0-9a-f]{7,128})\.[^.]+\z/, 1]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'set'
-
-
1
module Sprockets
-
-
# The purpose of this class is to generate a source map file
-
# that can be read and understood by browsers.
-
#
-
# When a file is passed in it will have a `application/js-sourcemap+json`
-
# or `application/css-sourcemap+json` mime type. The filename will be
-
# match the original asset. The original asset is loaded. As it
-
# gets processed by Sprockets it will aquire all information
-
# needed to build a source map file in the `asset.to_hash[:metadata][:map]`
-
# key.
-
#
-
# The output is an asset with a properly formatted source map file:
-
#
-
# {
-
# "version": 3,
-
# "sources": ["foo.js"],
-
# "names": [ ],
-
# "mappings": "AAAA,GAAIA"
-
# }
-
#
-
1
class SourceMapProcessor
-
1
def self.call(input)
-
links = Set.new(input[:metadata][:links])
-
env = input[:environment]
-
-
uri, _ = env.resolve!(input[:filename], accept: self.original_content_type(input[:content_type]))
-
asset = env.load(uri)
-
map = asset.metadata[:map]
-
-
# TODO: Because of the default piplene hack we have to apply dependencies
-
# from compiled asset to the source map, otherwise the source map cache
-
# will never detect the changes from directives
-
dependencies = Set.new(input[:metadata][:dependencies])
-
dependencies.merge(asset.metadata[:dependencies])
-
-
map["file"] = PathUtils.split_subpath(input[:load_path], input[:filename])
-
sources = map["sections"] ? map["sections"].map { |s| s["map"]["sources"] }.flatten : map["sources"]
-
-
sources.each do |source|
-
source = PathUtils.join(File.dirname(map["file"]), source)
-
uri, _ = env.resolve!(source)
-
links << uri
-
end
-
-
json = JSON.generate(map)
-
-
{ data: json, links: links, dependencies: dependencies }
-
end
-
-
1
def self.original_content_type(source_map_content_type, error_when_not_found: true)
-
5
case source_map_content_type
-
when "application/js-sourcemap+json"
-
"application/javascript"
-
when "application/css-sourcemap+json"
-
"text/css"
-
else
-
5
fail(source_map_content_type) if error_when_not_found
-
5
source_map_content_type
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'json'
-
1
require 'sprockets/path_utils'
-
-
1
module Sprockets
-
1
module SourceMapUtils
-
1
extend self
-
-
# Public: Transpose source maps into a standard format
-
#
-
# NOTE: Does not support index maps
-
#
-
# version => 3
-
# file => logical path
-
# sources => relative from filename
-
#
-
# Unnecessary attributes are removed
-
#
-
# Example
-
#
-
# map
-
# #=> {
-
# # "version" => 3,
-
# # "file" => "stdin",
-
# # "sourceRoot" => "",
-
# # "sourceContents" => "blah blah blah",
-
# # "sources" => [/root/logical/path.js],
-
# # "names" => [..],
-
# #}
-
# format_source_map(map, input)
-
# #=> {
-
# # "version" => 3,
-
# # "file" => "logical/path.js",
-
# # "sources" => ["path.js"],
-
# # "names" => [..],
-
# #}
-
1
def format_source_map(map, input)
-
2
filename = input[:filename]
-
2
load_path = input[:load_path]
-
2
load_paths = input[:environment].config[:paths]
-
2
mime_exts = input[:environment].config[:mime_exts]
-
2
pipeline_exts = input[:environment].config[:pipeline_exts]
-
2
file = PathUtils.split_subpath(load_path, filename)
-
{
-
2
"version" => 3,
-
"file" => file,
-
"mappings" => map["mappings"],
-
"sources" => map["sources"].map do |source|
-
10
source = URIUtils.split_file_uri(source)[2] if source.start_with? "file://"
-
10
source = PathUtils.join(File.dirname(filename), source) unless PathUtils.absolute_path?(source)
-
10
_, source = PathUtils.paths_split(load_paths, source)
-
10
source = PathUtils.relative_path_from(file, source)
-
10
PathUtils.set_pipeline(source, mime_exts, pipeline_exts, :source)
-
end,
-
"names" => map["names"]
-
}
-
end
-
-
# Public: Concatenate two source maps.
-
#
-
# For an example, if two js scripts are concatenated, the individual source
-
# maps for those files can be concatenated to map back to the originals.
-
#
-
# Examples
-
#
-
# script3 = "#{script1}#{script2}"
-
# map3 = concat_source_maps(map1, map2)
-
#
-
# a - Source map hash
-
# b - Source map hash
-
#
-
# Returns a new source map hash.
-
1
def concat_source_maps(a, b)
-
4
return a || b unless a && b
-
4
a = make_index_map(a)
-
4
b = make_index_map(b)
-
-
4
offset = 0
-
4
if a["sections"].count != 0 && !a["sections"].last["map"]["mappings"].empty?
-
2
last_line_count = a["sections"].last["map"].delete("x_sprockets_linecount")
-
2
offset += last_line_count || 1
-
-
2
last_offset = a["sections"].last["offset"]["line"]
-
2
offset += last_offset
-
end
-
-
4
a["sections"] += b["sections"].map do |section|
-
{
-
4
"offset" => section["offset"].merge({ "line" => section["offset"]["line"] + offset }),
-
"map" => section["map"].merge({
-
"sources" => section["map"]["sources"].map do |source|
-
12
PathUtils.relative_path_from(a["file"], PathUtils.join(File.dirname(b["file"]), source))
-
end
-
})
-
}
-
end
-
4
a
-
end
-
-
# Public: Converts source map to index map
-
#
-
# Example:
-
#
-
# map
-
# # => {
-
# "version" => 3,
-
# "file" => "..",
-
# "mappings" => "AAAA;AACA;..;AACA",
-
# "sources" => [..],
-
# "names" => [..]
-
# }
-
# make_index_map(map)
-
# # => {
-
# "version" => 3,
-
# "file" => "..",
-
# "sections" => [
-
# {
-
# "offset" => { "line" => 0, "column" => 0 },
-
# "map" => {
-
# "version" => 3,
-
# "file" => "..",
-
# "mappings" => "AAAA;AACA;..;AACA",
-
# "sources" => [..],
-
# "names" => [..]
-
# }
-
# }
-
# ]
-
# }
-
1
def make_index_map(map)
-
8
return map if map.key? "sections"
-
{
-
4
"version" => map["version"],
-
"file" => map["file"],
-
"sections" => [
-
{
-
"offset" => { "line" => 0, "column" => 0 },
-
"map" => map
-
}
-
]
-
}
-
end
-
-
# Public: Combine two seperate source map transformations into a single
-
# mapping.
-
#
-
# Source transformations may happen in discrete steps producing separate
-
# source maps. These steps can be combined into a single mapping back to
-
# the source.
-
#
-
# For an example, CoffeeScript may transform a file producing a map. Then
-
# Uglifier processes the result and produces another map. The CoffeeScript
-
# map can be combined with the Uglifier map so the source lines of the
-
# minified output can be traced back to the original CoffeeScript file.
-
#
-
# Returns a source map hash.
-
1
def combine_source_maps(first, second)
-
2
return second unless first
-
-
1
_first = decode_source_map(first)
-
1
_second = decode_source_map(second)
-
-
1
new_mappings = []
-
-
1
_second[:mappings].each do |m|
-
149
first_line = bsearch_mappings(_first[:mappings], m[:original])
-
149
new_mappings << first_line.merge(generated: m[:generated]) if first_line
-
end
-
-
1
_first[:mappings] = new_mappings
-
-
1
encode_source_map(_first)
-
end
-
-
# Public: Decompress source map
-
#
-
# Example:
-
#
-
# decode_source_map(map)
-
# # => {
-
# version: 3,
-
# file: "..",
-
# mappings: [
-
# { source: "..", generated: [0, 0], original: [0, 0], name: ".."}, ..
-
# ],
-
# sources: [..],
-
# names: [..]
-
# }
-
#
-
# map - Source map hash (v3 spec)
-
#
-
# Returns an uncompressed source map hash
-
1
def decode_source_map(map)
-
4
return nil unless map
-
-
4
mappings, sources, names = [], [], []
-
4
if map["sections"]
-
1
map["sections"].each do |s|
-
2
mappings += decode_source_map(s["map"])[:mappings].each do |m|
-
161
m[:generated][0] += s["offset"]["line"]
-
161
m[:generated][1] += s["offset"]["column"]
-
end
-
2
sources |= s["map"]["sources"]
-
2
names |= s["map"]["names"]
-
end
-
else
-
3
mappings = decode_vlq_mappings(map["mappings"], sources: map["sources"], names: map["names"])
-
3
sources = map["sources"]
-
3
names = map["names"]
-
end
-
{
-
4
version: 3,
-
file: map["file"],
-
mappings: mappings,
-
sources: sources,
-
names: names
-
}
-
end
-
-
# Public: Compress source map
-
#
-
# Example:
-
#
-
# encode_source_map(map)
-
# # => {
-
# "version" => 3,
-
# "file" => "..",
-
# "mappings" => "AAAA;AACA;..;AACA",
-
# "sources" => [..],
-
# "names" => [..]
-
# }
-
#
-
# map - Source map hash (uncompressed)
-
#
-
# Returns a compressed source map hash according to source map spec v3
-
1
def encode_source_map(map)
-
1
return nil unless map
-
{
-
1
"version" => map[:version],
-
"file" => map[:file],
-
"mappings" => encode_vlq_mappings(map[:mappings], sources: map[:sources], names: map[:names]),
-
"sources" => map[:sources],
-
"names" => map[:names]
-
}
-
end
-
-
# Public: Compare two source map offsets.
-
#
-
# Compatible with Array#sort.
-
#
-
# a - Array [line, column]
-
# b - Array [line, column]
-
#
-
# Returns -1 if a < b, 0 if a == b and 1 if a > b.
-
1
def compare_source_offsets(a, b)
-
939
diff = a[0] - b[0]
-
939
diff = a[1] - b[1] if diff == 0
-
-
939
if diff < 0
-
366
-1
-
573
elsif diff > 0
-
424
1
-
else
-
149
0
-
end
-
end
-
-
# Public: Search Array of mappings for closest offset.
-
#
-
# mappings - Array of mapping Hash objects
-
# offset - Array [line, column]
-
#
-
# Returns mapping Hash object.
-
1
def bsearch_mappings(mappings, offset, from = 0, to = mappings.size - 1)
-
939
mid = (from + to) / 2
-
-
939
if from > to
-
return from < 1 ? nil : mappings[from-1]
-
end
-
-
939
case compare_source_offsets(offset, mappings[mid][:generated])
-
when 0
-
149
mappings[mid]
-
when -1
-
366
bsearch_mappings(mappings, offset, from, mid - 1)
-
when 1
-
424
bsearch_mappings(mappings, offset, mid + 1, to)
-
end
-
end
-
-
# Public: Decode VLQ mappings and match up sources and symbol names.
-
#
-
# str - VLQ string from 'mappings' attribute
-
# sources - Array of Strings from 'sources' attribute
-
# names - Array of Strings from 'names' attribute
-
#
-
# Returns an Array of Mappings.
-
1
def decode_vlq_mappings(str, sources: [], names: [])
-
3
mappings = []
-
-
3
source_id = 0
-
3
original_line = 1
-
3
original_column = 0
-
3
name_id = 0
-
-
3
vlq_decode_mappings(str).each_with_index do |group, index|
-
35
generated_column = 0
-
35
generated_line = index + 1
-
-
35
group.each do |segment|
-
310
generated_column += segment[0]
-
310
generated = [generated_line, generated_column]
-
-
310
if segment.size >= 4
-
310
source_id += segment[1]
-
310
original_line += segment[2]
-
310
original_column += segment[3]
-
-
310
source = sources[source_id]
-
310
original = [original_line, original_column]
-
else
-
# TODO: Research this case
-
next
-
end
-
-
310
if segment[4]
-
name_id += segment[4]
-
name = names[name_id]
-
end
-
-
310
mapping = {source: source, generated: generated, original: original}
-
310
mapping[:name] = name if name
-
310
mappings << mapping
-
end
-
end
-
-
3
mappings
-
end
-
-
# Public: Encode mappings Hash into a VLQ encoded String.
-
#
-
# mappings - Array of Hash mapping objects
-
# sources - Array of String sources (default: mappings source order)
-
# names - Array of String names (default: mappings name order)
-
#
-
# Returns a VLQ encoded String.
-
1
def encode_vlq_mappings(mappings, sources: nil, names: nil)
-
1
sources ||= mappings.map { |m| m[:source] }.uniq.compact
-
1
names ||= mappings.map { |m| m[:name] }.uniq.compact
-
-
1
sources_index = Hash[sources.each_with_index.to_a]
-
1
names_index = Hash[names.each_with_index.to_a]
-
-
1
source_id = 0
-
1
source_line = 1
-
1
source_column = 0
-
1
name_id = 0
-
-
150
by_lines = mappings.group_by { |m| m[:generated][0] }
-
-
1
ary = (1..(by_lines.keys.max || 1)).map do |line|
-
1
generated_column = 0
-
-
1
(by_lines[line] || []).map do |mapping|
-
149
group = []
-
149
group << mapping[:generated][1] - generated_column
-
149
group << sources_index[mapping[:source]] - source_id
-
149
group << mapping[:original][0] - source_line
-
149
group << mapping[:original][1] - source_column
-
149
group << names_index[mapping[:name]] - name_id if mapping[:name]
-
-
149
generated_column = mapping[:generated][1]
-
149
source_id = sources_index[mapping[:source]]
-
149
source_line = mapping[:original][0]
-
149
source_column = mapping[:original][1]
-
149
name_id = names_index[mapping[:name]] if mapping[:name]
-
-
149
group
-
end
-
end
-
-
1
vlq_encode_mappings(ary)
-
end
-
-
# Public: Base64 VLQ encoding
-
#
-
# Adopted from ConradIrwin/ruby-source_map
-
# https://github.com/ConradIrwin/ruby-source_map/blob/master/lib/source_map/vlq.rb
-
#
-
# Resources
-
#
-
# http://en.wikipedia.org/wiki/Variable-length_quantity
-
# https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit
-
# https://github.com/mozilla/source-map/blob/master/lib/source-map/base64-vlq.js
-
#
-
1
VLQ_BASE_SHIFT = 5
-
1
VLQ_BASE = 1 << VLQ_BASE_SHIFT
-
1
VLQ_BASE_MASK = VLQ_BASE - 1
-
1
VLQ_CONTINUATION_BIT = VLQ_BASE
-
-
1
BASE64_DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')
-
65
BASE64_VALUES = (0...64).inject({}) { |h, i| h[BASE64_DIGITS[i]] = i; h }
-
-
# Public: Encode a list of numbers into a compact VLQ string.
-
#
-
# ary - An Array of Integers
-
#
-
# Returns a VLQ String.
-
1
def vlq_encode(ary)
-
149
result = []
-
149
ary.each do |n|
-
596
vlq = n < 0 ? ((-n) << 1) + 1 : n << 1
-
596
loop do
-
634
digit = vlq & VLQ_BASE_MASK
-
634
vlq >>= VLQ_BASE_SHIFT
-
634
digit |= VLQ_CONTINUATION_BIT if vlq > 0
-
634
result << BASE64_DIGITS[digit]
-
-
634
break unless vlq > 0
-
end
-
end
-
149
result.join
-
end
-
-
# Public: Decode a VLQ string.
-
#
-
# str - VLQ encoded String
-
#
-
# Returns an Array of Integers.
-
1
def vlq_decode(str)
-
310
result = []
-
310
shift = 0
-
310
value = 0
-
310
i = 0
-
-
310
while i < str.size do
-
1310
digit = BASE64_VALUES[str[i]]
-
1310
raise ArgumentError unless digit
-
1310
continuation = (digit & VLQ_CONTINUATION_BIT) != 0
-
1310
digit &= VLQ_BASE_MASK
-
1310
value += digit << shift
-
1310
if continuation
-
70
shift += VLQ_BASE_SHIFT
-
else
-
1240
result << ((value & 1) == 1 ? -(value >> 1) : value >> 1)
-
1240
value = shift = 0
-
end
-
1310
i += 1
-
end
-
310
result
-
end
-
-
# Public: Encode a mapping array into a compact VLQ string.
-
#
-
# ary - Two dimensional Array of Integers.
-
#
-
# Returns a VLQ encoded String seperated by , and ;.
-
1
def vlq_encode_mappings(ary)
-
1
ary.map { |group|
-
1
group.map { |segment|
-
149
vlq_encode(segment)
-
}.join(',')
-
}.join(';')
-
end
-
-
# Public: Decode a VLQ string into mapping numbers.
-
#
-
# str - VLQ encoded String
-
#
-
# Returns an two dimensional Array of Integers.
-
1
def vlq_decode_mappings(str)
-
3
mappings = []
-
-
3
str.split(';').each_with_index do |group, index|
-
35
mappings[index] = []
-
35
group.split(',').each do |segment|
-
310
mappings[index] << vlq_decode(segment)
-
end
-
end
-
-
3
mappings
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/http_utils'
-
1
require 'sprockets/processor_utils'
-
1
require 'sprockets/utils'
-
-
1
module Sprockets
-
1
module Transformers
-
1
include HTTPUtils, ProcessorUtils, Utils
-
-
# Public: Two level mapping of a source mime type to a target mime type.
-
#
-
# environment.transformers
-
# # => { 'text/coffeescript' => {
-
# 'application/javascript' => ConvertCoffeeScriptToJavaScript
-
# }
-
# }
-
#
-
1
def transformers
-
config[:transformers]
-
end
-
-
1
Transformer = Struct.new :from, :to, :proc
-
-
# Public: Register a transformer from and to a mime type.
-
#
-
# from - String mime type
-
# to - String mime type
-
# proc - Callable block that accepts an input Hash.
-
#
-
# Examples
-
#
-
# register_transformer 'text/coffeescript', 'application/javascript',
-
# ConvertCoffeeScriptToJavaScript
-
#
-
# register_transformer 'image/svg+xml', 'image/png', ConvertSvgToPng
-
#
-
# Returns nothing.
-
1
def register_transformer(from, to, proc)
-
21
self.config = hash_reassoc(config, :registered_transformers) do |transformers|
-
21
transformers << Transformer.new(from, to, proc)
-
end
-
21
compute_transformers!(self.config[:registered_transformers])
-
end
-
-
# Internal: Register transformer for existing type adding a suffix.
-
#
-
# types - Array of existing mime type Strings
-
# type_format - String suffix formatting string
-
# extname - String extension to append
-
# processor - Callable block that accepts an input Hash.
-
#
-
# Returns nothing.
-
1
def register_transformer_suffix(types, type_format, extname, processor)
-
1
Array(types).each do |type|
-
12
extensions, charset = mime_types[type].values_at(:extensions, :charset)
-
12
parts = type.split('/')
-
12
suffix_type = type_format.sub('\1', parts[0]).sub('\2', parts[1])
-
31
extensions = extensions.map { |ext| "#{ext}#{extname}" }
-
-
12
register_mime_type(suffix_type, extensions: extensions, charset: charset)
-
12
register_transformer(suffix_type, type, processor)
-
end
-
end
-
-
# Internal: Resolve target mime type that the source type should be
-
# transformed to.
-
#
-
# type - String from mime type
-
# accept - String accept type list (default: '*/*')
-
#
-
# Examples
-
#
-
# resolve_transform_type('text/plain', 'text/plain')
-
# # => 'text/plain'
-
#
-
# resolve_transform_type('image/svg+xml', 'image/png, image/*')
-
# # => 'image/png'
-
#
-
# resolve_transform_type('text/css', 'image/png')
-
# # => nil
-
#
-
# Returns String mime type or nil is no type satisfied the accept value.
-
1
def resolve_transform_type(type, accept)
-
6
find_best_mime_type_match(accept || '*/*', [type].compact + config[:transformers][type].keys)
-
end
-
-
# Internal: Expand accept type list to include possible transformed types.
-
#
-
# parsed_accepts - Array of accept q values
-
#
-
# Examples
-
#
-
# expand_transform_accepts([['application/javascript', 1.0]])
-
# # => [['application/javascript', 1.0], ['text/coffeescript', 0.8]]
-
#
-
# Returns an expanded Array of q values.
-
1
def expand_transform_accepts(parsed_accepts)
-
3
accepts = []
-
3
parsed_accepts.each do |(type, q)|
-
3
accepts.push([type, q])
-
3
config[:inverted_transformers][type].each do |subtype|
-
19
accepts.push([subtype, q * 0.8])
-
end
-
end
-
3
accepts
-
end
-
-
# Internal: Compose multiple transformer steps into a single processor
-
# function.
-
#
-
# transformers - Two level Hash of a source mime type to a target mime type
-
# types - Array of mime type steps
-
#
-
# Returns Processor.
-
1
def compose_transformers(transformers, types, preprocessors, postprocessors)
-
if types.length < 2
-
raise ArgumentError, "too few transform types: #{types.inspect}"
-
end
-
-
processors = types.each_cons(2).map { |src, dst|
-
unless processor = transformers[src][dst]
-
raise ArgumentError, "missing transformer for type: #{src} to #{dst}"
-
end
-
processor
-
}
-
-
compose_transformer_list processors, preprocessors, postprocessors
-
end
-
-
1
private
-
1
def compose_transformer_list(transformers, preprocessors, postprocessors)
-
581
processors = []
-
-
581
transformers.each do |processor|
-
942
processors.concat postprocessors[processor.from]
-
942
processors << processor.proc
-
942
processors.concat preprocessors[processor.to]
-
end
-
-
581
if processors.size > 1
-
438
compose_processors(*processors.reverse)
-
143
elsif processors.size == 1
-
143
processors.first
-
end
-
end
-
-
1
def compute_transformers!(registered_transformers)
-
29
preprocessors = self.config[:preprocessors]
-
29
postprocessors = self.config[:postprocessors]
-
29
transformers = Hash.new { {} }
-
29
inverted_transformers = Hash.new { Set.new }
-
29
incoming_edges = registered_transformers.group_by(&:from)
-
-
29
registered_transformers.each do |t|
-
883
traversals = dfs_paths([t]) { |k| incoming_edges.fetch(k.to, []) }
-
-
302
traversals.each do |nodes|
-
581
src, dst = nodes.first.from, nodes.last.to
-
581
processor = compose_transformer_list nodes, preprocessors, postprocessors
-
-
581
transformers[src] = {} unless transformers.key?(src)
-
581
transformers[src][dst] = processor
-
-
581
inverted_transformers[dst] = Set.new unless inverted_transformers.key?(dst)
-
581
inverted_transformers[dst] << src
-
end
-
end
-
-
58
self.config = hash_reassoc(config, :transformers) { transformers }
-
58
self.config = hash_reassoc(config, :inverted_transformers) { inverted_transformers }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/autoload'
-
1
require 'sprockets/digest_utils'
-
1
require 'sprockets/source_map_utils'
-
-
1
module Sprockets
-
# Public: Uglifier/Uglify compressor.
-
#
-
# To accept the default options
-
#
-
# environment.register_bundle_processor 'application/javascript',
-
# Sprockets::UglifierCompressor
-
#
-
# Or to pass options to the Uglifier class.
-
#
-
# environment.register_bundle_processor 'application/javascript',
-
# Sprockets::UglifierCompressor.new(comments: :copyright)
-
#
-
1
class UglifierCompressor
-
1
VERSION = '3'
-
-
# Public: Return singleton instance with default options.
-
#
-
# Returns UglifierCompressor object.
-
1
def self.instance
-
@instance ||= new
-
end
-
-
1
def self.call(input)
-
instance.call(input)
-
end
-
-
1
def self.cache_key
-
instance.cache_key
-
end
-
-
1
attr_reader :cache_key
-
-
1
def initialize(options = {})
-
options[:comments] ||= :none
-
-
@options = options
-
@cache_key = "#{self.class.name}:#{Autoload::Uglifier::VERSION}:#{VERSION}:#{DigestUtils.digest(options)}".freeze
-
end
-
-
1
def call(input)
-
case Autoload::Uglifier::VERSION.to_i
-
when 1
-
raise "uglifier 1.x is no longer supported, please upgrade to 2.x or newer"
-
when 2
-
input_options = { source_filename: input[:filename] }
-
else
-
input_options = { source_map: { filename: input[:filename] } }
-
end
-
-
uglifier = Autoload::Uglifier.new(@options.merge(input_options))
-
-
js, map = uglifier.compile_with_map(input[:data])
-
-
map = SourceMapUtils.format_source_map(JSON.parse(map), input)
-
map = SourceMapUtils.combine_source_maps(input[:metadata][:map], map)
-
-
{ data: js, map: map }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/uri_utils'
-
1
require 'sprockets/uri_tar'
-
-
1
module Sprockets
-
# Internal: Used to parse and store the URI to an unloaded asset
-
# Generates keys used to store and retrieve items from cache
-
1
class UnloadedAsset
-
-
# Internal: Initialize object for generating cache keys
-
#
-
# uri - A String containing complete URI to a file including scheme
-
# and full path such as
-
# "file:///Path/app/assets/js/app.js?type=application/javascript"
-
# env - The current "environment" that assets are being loaded into.
-
# We need it so we know where the +root+ (directory where Sprockets
-
# is being invoked). We also need it for the `file_digest` method,
-
# since, for some strange reason, memoization is provided by
-
# overriding methods such as `stat` in the `PathUtils` module.
-
#
-
# Returns UnloadedAsset.
-
1
def initialize(uri, env)
-
32
@uri = uri.to_s
-
32
@env = env
-
32
@compressed_path = URITar.new(uri, env).compressed_path
-
32
@params = nil # lazy loaded
-
32
@filename = nil # lazy loaded
-
end
-
1
attr_reader :compressed_path, :uri
-
-
# Internal: Full file path without schema
-
#
-
# This returns a string containing the full path to the asset without the schema.
-
# Information is loaded lazily since we want `UnloadedAsset.new(dep, self).relative_path`
-
# to be fast. Calling this method the first time allocates an array and a hash.
-
#
-
# Example
-
#
-
# If the URI is `file:///Full/path/app/assets/javascripts/application.js"` then the
-
# filename would be `"/Full/path/app/assets/javascripts/application.js"`
-
#
-
# Returns a String.
-
1
def filename
-
36
unless @filename
-
load_file_params
-
end
-
36
@filename
-
end
-
-
# Internal: Hash of param values
-
#
-
# This information is generated and used internally by Sprockets.
-
# Known keys include `:type` which stores the asset's mime-type, `:id` which is a fully resolved
-
# digest for the asset (includes dependency digest as opposed to a digest of only file contents)
-
# and `:pipeline`. Hash may be empty.
-
#
-
# Example
-
#
-
# If the URI is `file:///Full/path/app/assets/javascripts/application.js"type=application/javascript`
-
# Then the params would be `{type: "application/javascript"}`
-
#
-
# Returns a Hash.
-
1
def params
-
30
unless @params
-
6
load_file_params
-
end
-
30
@params
-
end
-
-
# Internal: Key of asset
-
#
-
# Used to retrieve an asset from the cache based on "compressed" path to asset.
-
# A "compressed" path can either be relative to the root of the project or an
-
# absolute path.
-
#
-
# Returns a String.
-
1
def asset_key
-
6
"asset-uri:#{compressed_path}"
-
end
-
-
# Public: Dependency History key
-
#
-
# Used to retrieve an array of "histories" each of which contains a set of stored dependencies
-
# for a given asset path and filename digest.
-
#
-
# A dependency can refer to either an asset e.g. index.js
-
# may rely on jquery.js (so jquery.js is a dependency), or other factors that may affect
-
# compilation, such as the VERSION of Sprockets (i.e. the environment) and what "processors"
-
# are used.
-
#
-
# For example a history array with one Set of dependencies may look like:
-
#
-
# [["environment-version", "environment-paths", "processors:type=text/css&file_type=text/css",
-
# "file-digest:///Full/path/app/assets/stylesheets/application.css",
-
# "processors:type=text/css&file_type=text/css&pipeline=self",
-
# "file-digest:///Full/path/app/assets/stylesheets"]]
-
#
-
# This method of asset lookup is used to ensure that none of the dependencies have been modified
-
# since last lookup. If one of them has, the key will be different and a new entry must be stored.
-
#
-
# URI dependencies are later converted to "compressed" paths
-
#
-
# Returns a String.
-
1
def dependency_history_key
-
6
"asset-uri-cache-dependencies:#{compressed_path}:#{ @env.file_digest(filename) }"
-
end
-
-
# Internal: Digest key
-
#
-
# Used to retrieve a string containing the "compressed" path to an asset based on
-
# a digest. The digest is generated from dependencies stored via information stored in
-
# the `dependency_history_key` after each of the "dependencies" is "resolved".
-
# For example "environment-version" may be resolved to "environment-1.0-3.2.0"
-
# for version "3.2.0" of Sprockets
-
#
-
# Returns a String.
-
1
def digest_key(digest)
-
6
"asset-uri-digest:#{compressed_path}:#{digest}"
-
end
-
-
# Internal: File digest key
-
#
-
# The digest for a given file won't change if the path and the stat time hasn't changed
-
# We can save time by not re-computing this information and storing it in the cache
-
#
-
# Returns a String.
-
1
def file_digest_key(stat)
-
20
"file_digest:#{compressed_path}:#{stat}"
-
end
-
-
1
private
-
# Internal: Parses uri into filename and params hash
-
#
-
# Returns Array with filename and params hash
-
1
def load_file_params
-
6
@filename, @params = URIUtils.parse_asset_uri(uri)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/path_utils'
-
-
1
module Sprockets
-
# Internal: used to "expand" and "compress" values for storage
-
1
class URITar
-
1
attr_reader :scheme, :root, :path
-
-
# Internal: Initialize object for compression or expansion
-
#
-
# uri - A String containing URI that may or may not contain the scheme
-
# env - The current "environment" that assets are being loaded into.
-
1
def initialize(uri, env)
-
124
@root = env.root
-
124
@env = env
-
124
uri = uri.to_s
-
124
if uri.include?("://".freeze)
-
88
@scheme, _, @path = uri.partition("://".freeze)
-
88
@scheme << "://".freeze
-
else
-
36
@scheme = "".freeze
-
36
@path = uri
-
end
-
end
-
-
# Internal: Converts full uri to a "compressed" uri
-
#
-
# If a uri is inside of an environment's root it will
-
# be shortened to be a relative path.
-
#
-
# If a uri is outside of the environment's root the original
-
# uri will be returned.
-
#
-
# Returns String
-
1
def compress
-
92
scheme + compressed_path
-
end
-
-
# Internal: Tells us if we are using an absolute path
-
#
-
# Nix* systems start with a `/` like /Users/schneems.
-
# Windows systems start with a drive letter than colon and slash
-
# like C:/Schneems.
-
1
def absolute_path?
-
PathUtils.absolute_path?(path)
-
end
-
-
# Internal: Convert a "compressed" uri to an absolute path
-
#
-
# If a uri is inside of the environment's root it will not
-
# start with a slash for example:
-
#
-
# file://this/is/a/relative/path
-
#
-
# If a uri is outside the root, it will start with a slash:
-
#
-
# file:///This/is/an/absolute/path
-
#
-
# Returns String
-
1
def expand
-
if absolute_path?
-
# Stored path was absolute, don't add root
-
scheme + path
-
else
-
if scheme.empty?
-
File.join(root, path)
-
else
-
# We always want to return an absolute uri,
-
# make sure the path starts with a slash.
-
scheme + File.join("/".freeze, root, path)
-
end
-
end
-
end
-
-
# Internal: Returns "compressed" path
-
#
-
# If the input uri is relative to the environment root
-
# it will return a path relative to the environment root.
-
# Otherwise an absolute path will be returned.
-
#
-
# Only path information is returned, and not scheme.
-
#
-
# Returns String
-
1
def compressed_path
-
# windows
-
124
if !@root.start_with?("/".freeze) && path.start_with?("/".freeze)
-
consistent_root = "/".freeze + @root
-
else
-
124
consistent_root = @root
-
end
-
-
124
if compressed_path = PathUtils.split_subpath(consistent_root, path)
-
124
compressed_path
-
else
-
path
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'uri'
-
-
1
module Sprockets
-
# Internal: Asset URI related parsing utilities. Mixed into Environment.
-
#
-
# An Asset URI identifies the compiled Asset result. It shares the file:
-
# scheme and requires an absolute path.
-
#
-
# Other query parameters
-
#
-
# type - String output content type. Otherwise assumed from file extension.
-
# This maybe different than the extension if the asset is transformed
-
# from one content type to another. For an example .coffee -> .js.
-
#
-
# id - Unique fingerprint of the entire asset and all its metadata. Assets
-
# will only have the same id if they serialize to an identical value.
-
#
-
# pipeline - String name of pipeline.
-
#
-
1
module URIUtils
-
1
extend self
-
-
# Internal: Parse URI into component parts.
-
#
-
# uri - String uri
-
#
-
# Returns Array of components.
-
1
def split_uri(uri)
-
URI.split(uri)
-
end
-
-
# Internal: Join URI component parts into String.
-
#
-
# Returns String.
-
1
def join_uri(scheme, userinfo, host, port, registry, path, opaque, query, fragment)
-
URI::Generic.new(scheme, userinfo, host, port, registry, path, opaque, query, fragment).to_s
-
end
-
-
# Internal: Parse file: URI into component parts.
-
#
-
# uri - String uri
-
#
-
# Returns [scheme, host, path, query].
-
1
def split_file_uri(uri)
-
26
scheme, _, host, _, _, path, _, query, _ = URI.split(uri)
-
-
26
path = URI::Generic::DEFAULT_PARSER.unescape(path)
-
26
path.force_encoding(Encoding::UTF_8)
-
-
# Hack for parsing Windows "/C:/Users/IEUser" paths
-
26
if File::ALT_SEPARATOR && path[2] == ':'
-
path = path[1..-1]
-
end
-
-
26
[scheme, host, path, query]
-
end
-
-
# Internal: Join file: URI component parts into String.
-
#
-
# Returns String.
-
1
def join_file_uri(scheme, host, path, query)
-
39
str = +"#{scheme}://"
-
39
str << host if host
-
39
path = "/#{path}" unless path.start_with?("/".freeze)
-
39
str << URI::Generic::DEFAULT_PARSER.escape(path)
-
39
str << "?#{query}" if query
-
39
str
-
end
-
-
# Internal: Check if String is a valid Asset URI.
-
#
-
# str - Possible String asset URI.
-
#
-
# Returns true or false.
-
1
def valid_asset_uri?(str)
-
# Quick prefix check before attempting a full parse
-
6
str.start_with?("file://".freeze) && parse_asset_uri(str) ? true : false
-
rescue URI::InvalidURIError
-
false
-
end
-
-
# Internal: Parse Asset URI.
-
#
-
# Examples
-
#
-
# parse("file:///tmp/js/application.coffee?type=application/javascript")
-
# # => "/tmp/js/application.coffee", {type: "application/javascript"}
-
#
-
# uri - String asset URI
-
#
-
# Returns String path and Hash of symbolized parameters.
-
1
def parse_asset_uri(uri)
-
10
scheme, _, path, query = split_file_uri(uri)
-
-
10
unless scheme == 'file'
-
raise URI::InvalidURIError, "expected file:// scheme: #{uri}"
-
end
-
-
10
return path, parse_uri_query_params(query)
-
end
-
-
# Internal: Build Asset URI.
-
#
-
# Examples
-
#
-
# build("/tmp/js/application.coffee", type: "application/javascript")
-
# # => "file:///tmp/js/application.coffee?type=application/javascript"
-
#
-
# path - String file path
-
# params - Hash of optional parameters
-
#
-
# Returns String URI.
-
1
def build_asset_uri(path, params = {})
-
12
join_file_uri("file", nil, path, encode_uri_query_params(params))
-
end
-
-
# Internal: Parse file-digest dependency URI.
-
#
-
# Examples
-
#
-
# parse("file-digest:/tmp/js/application.js")
-
# # => "/tmp/js/application.js"
-
#
-
# uri - String file-digest URI
-
#
-
# Returns String path.
-
1
def parse_file_digest_uri(uri)
-
16
scheme, _, path, _ = split_file_uri(uri)
-
-
16
unless scheme == 'file-digest'.freeze
-
raise URI::InvalidURIError, "expected file-digest scheme: #{uri}"
-
end
-
-
16
path
-
end
-
-
# Internal: Build file-digest dependency URI.
-
#
-
# Examples
-
#
-
# build("/tmp/js/application.js")
-
# # => "file-digest:/tmp/js/application.js"
-
#
-
# path - String file path
-
#
-
# Returns String URI.
-
1
def build_file_digest_uri(path)
-
27
join_file_uri('file-digest'.freeze, nil, path, nil)
-
end
-
-
# Internal: Serialize hash of params into query string.
-
#
-
# params - Hash of params to serialize
-
#
-
# Returns String query or nil if empty.
-
1
def encode_uri_query_params(params)
-
18
query = []
-
-
18
params.each do |key, value|
-
52
case value
-
when Integer
-
query << "#{key}=#{value}"
-
when String, Symbol
-
42
query << "#{key}=#{URI::Generic::DEFAULT_PARSER.escape(value.to_s)}"
-
when TrueClass
-
query << "#{key}"
-
when FalseClass, NilClass
-
else
-
raise TypeError, "unexpected type: #{value.class}"
-
end
-
end
-
-
18
"#{query.join('&'.freeze)}" if query.any?
-
end
-
-
# Internal: Parse query string into hash of params
-
#
-
# query - String query string
-
#
-
# Return Hash of params.
-
1
def parse_uri_query_params(query)
-
15
query.to_s.split('&'.freeze).reduce({}) do |h, p|
-
31
k, v = p.split('='.freeze, 2)
-
31
v = URI::Generic::DEFAULT_PARSER.unescape(v) if v
-
31
h[k.to_sym] = v || true
-
31
h
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'set'
-
-
1
module Sprockets
-
# Internal: Utils, we didn't know where else to put it! Functions may
-
# eventually be shuffled into more specific drawers.
-
1
module Utils
-
1
extend self
-
-
# Internal: Check if object can safely be .dup'd.
-
#
-
# Similar to ActiveSupport #duplicable? check.
-
#
-
# obj - Any Object
-
#
-
# Returns false if .dup would raise a TypeError, otherwise true.
-
1
def duplicable?(obj)
-
546
case obj
-
when NilClass, FalseClass, TrueClass, Symbol, Numeric
-
1
false
-
else
-
545
true
-
end
-
end
-
-
# Internal: Duplicate and store key/value on new frozen hash.
-
#
-
# Seperated for recursive calls, always use hash_reassoc(hash, *keys).
-
#
-
# hash - Hash
-
# key - Object key
-
#
-
# Returns Hash.
-
1
def hash_reassoc1(hash, key)
-
273
hash = hash.dup if hash.frozen?
-
273
old_value = hash[key]
-
273
old_value = old_value.dup if duplicable?(old_value)
-
273
new_value = yield old_value
-
273
new_value.freeze if duplicable?(new_value)
-
273
hash.store(key, new_value)
-
273
hash.freeze
-
end
-
-
# Internal: Duplicate and store key/value on new frozen hash.
-
#
-
# Similar to Hash#store for nested frozen hashes.
-
#
-
# hash - Hash
-
# key_a - Object key. Use multiple keys for nested hashes.
-
# key_b - Object key. Use multiple keys for nested hashes.
-
# block - Receives current value at key.
-
#
-
# Examples
-
#
-
# config = {paths: ["/bin", "/sbin"]}.freeze
-
# new_config = hash_reassoc(config, :paths) do |paths|
-
# paths << "/usr/local/bin"
-
# end
-
#
-
# Returns duplicated frozen Hash.
-
1
def hash_reassoc(hash, key_a, key_b = nil, &block)
-
273
if key_b
-
32
hash_reassoc1(hash, key_a) do |value|
-
32
hash_reassoc(value, key_b, &block)
-
end
-
else
-
241
hash_reassoc1(hash, key_a, &block)
-
end
-
end
-
-
1
WHITESPACE_ORDINALS = {0x0A => "\n", 0x20 => " ", 0x09 => "\t"}
-
1
private_constant :WHITESPACE_ORDINALS
-
-
# Internal: Check if string has a trailing semicolon.
-
#
-
# str - String
-
#
-
# Returns true or false.
-
1
def string_end_with_semicolon?(str)
-
1
i = str.size - 1
-
1
while i >= 0
-
2
c = str[i].ord
-
2
i -= 1
-
-
2
next if WHITESPACE_ORDINALS[c]
-
-
1
return c === 0x3B
-
end
-
-
true
-
end
-
-
# Internal: Accumulate asset source to buffer and append a trailing
-
# semicolon if necessary.
-
#
-
# buf - String buffer to append to
-
# source - String source to append
-
#
-
# Returns buf String.
-
1
def concat_javascript_sources(buf, source)
-
2
return buf if source.bytesize <= 0
-
-
1
buf << source
-
# If the source contains non-ASCII characters, indexing on it becomes O(N).
-
# This will lead to O(N^2) performance in string_end_with_semicolon?, so we should use 32 bit encoding to make sure indexing stays O(1)
-
1
source = source.encode(Encoding::UTF_32LE) unless source.ascii_only?
-
1
return buf if string_end_with_semicolon?(source)
-
-
# If the last character in the string was whitespace,
-
# such as a newline, then we want to put the semicolon
-
# before the whitespace. Otherwise append a semicolon.
-
if whitespace = WHITESPACE_ORDINALS[source[-1].ord]
-
buf[-1] = ";#{whitespace}"
-
else
-
buf << ";"
-
end
-
-
buf
-
end
-
-
# Internal: Inject into target module for the duration of the block.
-
#
-
# mod - Module
-
#
-
# Returns result of block.
-
1
def module_include(base, mod)
-
1
old_methods = {}
-
-
1
mod.instance_methods.each do |sym|
-
18
old_methods[sym] = base.instance_method(sym) if base.method_defined?(sym)
-
end
-
-
1
mod.instance_methods.each do |sym|
-
18
method = mod.instance_method(sym)
-
18
base.send(:define_method, sym, method)
-
end
-
-
1
yield
-
ensure
-
1
mod.instance_methods.each do |sym|
-
18
base.send(:undef_method, sym) if base.method_defined?(sym)
-
end
-
1
old_methods.each do |sym, method|
-
base.send(:define_method, sym, method)
-
end
-
end
-
-
# Internal: Post-order Depth-First search algorithm.
-
#
-
# Used for resolving asset dependencies.
-
#
-
# initial - Initial Array of nodes to traverse.
-
# block -
-
# node - Current node to get children of
-
#
-
# Returns a Set of nodes.
-
1
def dfs(initial)
-
4
nodes, seen = Set.new, Set.new
-
4
stack = Array(initial).reverse
-
-
4
while node = stack.pop
-
8
if seen.include?(node)
-
4
nodes.add(node)
-
else
-
4
seen.add(node)
-
4
stack.push(node)
-
4
stack.concat(Array(yield node).reverse)
-
end
-
end
-
-
4
nodes
-
end
-
-
# Internal: Post-order Depth-First search algorithm that gathers all paths
-
# along the way.
-
#
-
# TODO: Rename function.
-
#
-
# path - Initial Array node path
-
# block -
-
# node - Current node to get children of
-
#
-
# Returns an Array of node Arrays.
-
1
def dfs_paths(path)
-
302
paths = []
-
302
stack = [path]
-
302
seen = Set.new
-
-
302
while path = stack.pop
-
581
seen.add(path.last)
-
581
paths << path
-
-
581
children = yield path.last
-
581
children.reverse_each do |node|
-
279
stack.push(path + [node]) unless seen.include?(node)
-
end
-
end
-
-
302
paths
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Sprockets
-
1
module Utils
-
1
class Gzip
-
# Private: Generates a gzipped file based off of reference asset.
-
#
-
# ZlibArchiver.call(file, source, mtime)
-
#
-
# Compresses a given `source` using stdlib Zlib algorithm
-
# writes contents to the `file` passed in. Sets `mtime` of
-
# written file to passed in `mtime`
-
1
module ZlibArchiver
-
1
def self.call(file, source, mtime)
-
gz = Zlib::GzipWriter.new(file, Zlib::BEST_COMPRESSION)
-
gz.mtime = mtime
-
gz.write(source)
-
gz.close
-
-
File.utime(mtime, mtime, file.path)
-
end
-
end
-
-
# Private: Generates a gzipped file based off of reference asset.
-
#
-
# ZopfliArchiver.call(file, source, mtime)
-
#
-
# Compresses a given `source` using the zopfli gem
-
# writes contents to the `file` passed in. Sets `mtime` of
-
# written file to passed in `mtime`
-
1
module ZopfliArchiver
-
1
def self.call(file, source, mtime)
-
compressed_source = Autoload::Zopfli.deflate(source, format: :gzip, mtime: mtime)
-
file.write(compressed_source)
-
file.close
-
-
nil
-
end
-
end
-
-
1
attr_reader :content_type, :source, :charset, :archiver
-
-
# Private: Generates a gzipped file based off of reference file.
-
1
def initialize(asset, archiver: ZlibArchiver)
-
@content_type = asset.content_type
-
@source = asset.source
-
@charset = asset.charset
-
@archiver = archiver
-
end
-
-
# What non-text mime types should we compress? This list comes from:
-
# https://www.fastly.com/blog/new-gzip-settings-and-deciding-what-compress
-
1
COMPRESSABLE_MIME_TYPES = {
-
"application/vnd.ms-fontobject" => true,
-
"application/x-font-opentype" => true,
-
"application/x-font-ttf" => true,
-
"image/x-icon" => true,
-
"image/svg+xml" => true
-
}
-
-
# Private: Returns whether or not an asset can be compressed.
-
#
-
# We want to compress any file that is text based.
-
# You do not want to compress binary
-
# files as they may already be compressed and running them
-
# through a compression algorithm would make them larger.
-
#
-
# Return Boolean.
-
1
def can_compress?
-
# The "charset" of a mime type is present if the value is
-
# encoded text. We can check this value to see if the asset
-
# can be compressed.
-
#
-
# We also check against our list of non-text compressible mime types
-
@charset || COMPRESSABLE_MIME_TYPES.include?(@content_type)
-
end
-
-
# Private: Opposite of `can_compress?`.
-
#
-
# Returns Boolean.
-
1
def cannot_compress?
-
!can_compress?
-
end
-
-
# Private: Generates a gzipped file based off of reference asset.
-
#
-
# Compresses the target asset's contents and puts it into a file with
-
# the same name plus a `.gz` extension in the same folder as the original.
-
# Does not modify the target asset.
-
#
-
# Returns nothing.
-
1
def compress(file, target)
-
mtime = Sprockets::PathUtils.stat(target).mtime
-
archiver.call(file, source, mtime)
-
-
nil
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Sprockets
-
1
VERSION = "4.0.2"
-
end
-
# frozen_string_literal: true
-
1
require 'sprockets/autoload'
-
1
require 'sprockets/digest_utils'
-
-
1
module Sprockets
-
# Public: YUI compressor.
-
#
-
# To accept the default options
-
#
-
# environment.register_bundle_processor 'application/javascript',
-
# Sprockets::YUICompressor
-
#
-
# Or to pass options to the YUI::JavaScriptCompressor class.
-
#
-
# environment.register_bundle_processor 'application/javascript',
-
# Sprockets::YUICompressor.new(munge: true)
-
#
-
1
class YUICompressor
-
1
VERSION = '1'
-
-
# Public: Return singleton instance with default options.
-
#
-
# Returns YUICompressor object.
-
1
def self.instance
-
@instance ||= new
-
end
-
-
1
def self.call(input)
-
instance.call(input)
-
end
-
-
1
def self.cache_key
-
instance.cache_key
-
end
-
-
1
attr_reader :cache_key
-
-
1
def initialize(options = {})
-
@options = options
-
@cache_key = "#{self.class.name}:#{Autoload::YUI::Compressor::VERSION}:#{VERSION}:#{DigestUtils.digest(options)}".freeze
-
end
-
-
1
def call(input)
-
data = input[:data]
-
-
case input[:content_type]
-
when 'application/javascript'
-
Autoload::YUI::JavaScriptCompressor.new(@options).compress(data)
-
when 'text/css'
-
Autoload::YUI::CssCompressor.new(@options).compress(data)
-
else
-
data
-
end
-
end
-
end
-
end
-
1
require 'tilt/mapping'
-
1
require 'tilt/template'
-
-
# Namespace for Tilt. This module is not intended to be included anywhere.
-
1
module Tilt
-
# Current version.
-
1
VERSION = '2.0.10'
-
-
1
@default_mapping = Mapping.new
-
-
# @return [Tilt::Mapping] the main mapping object
-
1
def self.default_mapping
-
42
@default_mapping
-
end
-
-
# @private
-
1
def self.lazy_map
-
default_mapping.lazy_map
-
end
-
-
# @see Tilt::Mapping#register
-
1
def self.register(template_class, *extensions)
-
default_mapping.register(template_class, *extensions)
-
end
-
-
# @see Tilt::Mapping#register_lazy
-
1
def self.register_lazy(class_name, file, *extensions)
-
42
default_mapping.register_lazy(class_name, file, *extensions)
-
end
-
-
# @deprecated Use {register} instead.
-
1
def self.prefer(template_class, *extensions)
-
register(template_class, *extensions)
-
end
-
-
# @see Tilt::Mapping#registered?
-
1
def self.registered?(ext)
-
default_mapping.registered?(ext)
-
end
-
-
# @see Tilt::Mapping#new
-
1
def self.new(file, line=nil, options={}, &block)
-
default_mapping.new(file, line, options, &block)
-
end
-
-
# @see Tilt::Mapping#[]
-
1
def self.[](file)
-
default_mapping[file]
-
end
-
-
# @see Tilt::Mapping#template_for
-
1
def self.template_for(file)
-
default_mapping.template_for(file)
-
end
-
-
# @see Tilt::Mapping#templates_for
-
1
def self.templates_for(file)
-
default_mapping.templates_for(file)
-
end
-
-
# @return the template object that is currently rendering.
-
#
-
# @example
-
# tmpl = Tilt['index.erb'].new { '<%= Tilt.current_template %>' }
-
# tmpl.render == tmpl.to_s
-
#
-
# @note This is currently an experimental feature and might return nil
-
# in the future.
-
1
def self.current_template
-
Thread.current[:tilt_current_template]
-
end
-
-
# Extremely simple template cache implementation. Calling applications
-
# create a Tilt::Cache instance and use #fetch with any set of hashable
-
# arguments (such as those to Tilt.new):
-
#
-
# cache = Tilt::Cache.new
-
# cache.fetch(path, line, options) { Tilt.new(path, line, options) }
-
#
-
# Subsequent invocations return the already loaded template object.
-
#
-
# @note
-
# Tilt::Cache is a thin wrapper around Hash. It has the following
-
# limitations:
-
# * Not thread-safe.
-
# * Size is unbounded.
-
# * Keys are not copied defensively, and should not be modified after
-
# being passed to #fetch. More specifically, the values returned by
-
# key#hash and key#eql? should not change.
-
# If this is too limiting for you, use a different cache implementation.
-
1
class Cache
-
1
def initialize
-
2
@cache = {}
-
end
-
-
# Caches a value for key, or returns the previously cached value.
-
# If a value has been previously cached for key then it is
-
# returned. Otherwise, block is yielded to and its return value
-
# which may be nil, is cached under key and returned.
-
# @yield
-
# @yieldreturn the value to cache for key
-
1
def fetch(*key)
-
@cache.fetch(key) do
-
@cache[key] = yield
-
end
-
end
-
-
# Clears the cache.
-
1
def clear
-
@cache = {}
-
end
-
end
-
-
-
# Template Implementations ================================================
-
-
# ERB
-
1
register_lazy :ERBTemplate, 'tilt/erb', 'erb', 'rhtml'
-
1
register_lazy :ErubisTemplate, 'tilt/erubis', 'erb', 'rhtml', 'erubis'
-
1
register_lazy :ErubiTemplate, 'tilt/erubi', 'erb', 'rhtml', 'erubi'
-
-
# Markdown
-
1
register_lazy :BlueClothTemplate, 'tilt/bluecloth', 'markdown', 'mkd', 'md'
-
1
register_lazy :MarukuTemplate, 'tilt/maruku', 'markdown', 'mkd', 'md'
-
1
register_lazy :KramdownTemplate, 'tilt/kramdown', 'markdown', 'mkd', 'md'
-
1
register_lazy :RDiscountTemplate, 'tilt/rdiscount', 'markdown', 'mkd', 'md'
-
1
register_lazy :RedcarpetTemplate, 'tilt/redcarpet', 'markdown', 'mkd', 'md'
-
1
register_lazy :CommonMarkerTemplate, 'tilt/commonmarker', 'markdown', 'mkd', 'md'
-
1
register_lazy :PandocTemplate, 'tilt/pandoc', 'markdown', 'mkd', 'md'
-
-
# Rest (sorted by name)
-
1
register_lazy :AsciidoctorTemplate, 'tilt/asciidoc', 'ad', 'adoc', 'asciidoc'
-
1
register_lazy :BabelTemplate, 'tilt/babel', 'es6', 'babel', 'jsx'
-
1
register_lazy :BuilderTemplate, 'tilt/builder', 'builder'
-
1
register_lazy :CSVTemplate, 'tilt/csv', 'rcsv'
-
1
register_lazy :CoffeeScriptTemplate, 'tilt/coffee', 'coffee'
-
1
register_lazy :CoffeeScriptLiterateTemplate, 'tilt/coffee', 'litcoffee'
-
1
register_lazy :CreoleTemplate, 'tilt/creole', 'wiki', 'creole'
-
1
register_lazy :EtanniTemplate, 'tilt/etanni', 'etn', 'etanni'
-
1
register_lazy :HamlTemplate, 'tilt/haml', 'haml'
-
1
register_lazy :LessTemplate, 'tilt/less', 'less'
-
1
register_lazy :LiquidTemplate, 'tilt/liquid', 'liquid'
-
1
register_lazy :LiveScriptTemplate, 'tilt/livescript','ls'
-
1
register_lazy :MarkabyTemplate, 'tilt/markaby', 'mab'
-
1
register_lazy :NokogiriTemplate, 'tilt/nokogiri', 'nokogiri'
-
1
register_lazy :PlainTemplate, 'tilt/plain', 'html'
-
1
register_lazy :PrawnTemplate, 'tilt/prawn', 'prawn'
-
1
register_lazy :RDocTemplate, 'tilt/rdoc', 'rdoc'
-
1
register_lazy :RadiusTemplate, 'tilt/radius', 'radius'
-
1
register_lazy :RedClothTemplate, 'tilt/redcloth', 'textile'
-
1
register_lazy :RstPandocTemplate, 'tilt/rst-pandoc', 'rst'
-
1
register_lazy :SassTemplate, 'tilt/sass', 'sass'
-
1
register_lazy :ScssTemplate, 'tilt/sass', 'scss'
-
1
register_lazy :SigilTemplate, 'tilt/sigil', 'sigil'
-
1
register_lazy :StringTemplate, 'tilt/string', 'str'
-
1
register_lazy :TypeScriptTemplate, 'tilt/typescript', 'ts', 'tsx'
-
1
register_lazy :WikiClothTemplate, 'tilt/wikicloth', 'wiki', 'mediawiki', 'mw'
-
1
register_lazy :YajlTemplate, 'tilt/yajl', 'yajl'
-
-
# External template engines
-
1
register_lazy 'Slim::Template', 'slim', 'slim'
-
1
register_lazy 'Tilt::HandlebarsTemplate', 'tilt/handlebars', 'handlebars', 'hbs'
-
1
register_lazy 'Tilt::OrgTemplate', 'org-ruby', 'org'
-
1
register_lazy 'Opal::Processor', 'opal', 'opal', 'rb'
-
1
register_lazy 'Tilt::JbuilderTemplate', 'tilt/jbuilder', 'jbuilder'
-
end
-
# Used for detecting autoloading bug in JRuby
-
1
class Tilt::Dummy; end
-
-
1
require 'monitor'
-
-
1
module Tilt
-
# Tilt::Mapping associates file extensions with template implementations.
-
#
-
# mapping = Tilt::Mapping.new
-
# mapping.register(Tilt::RDocTemplate, 'rdoc')
-
# mapping['index.rdoc'] # => Tilt::RDocTemplate
-
# mapping.new('index.rdoc').render
-
#
-
# You can use {#register} to register a template class by file
-
# extension, {#registered?} to see if a file extension is mapped,
-
# {#[]} to lookup template classes, and {#new} to instantiate template
-
# objects.
-
#
-
# Mapping also supports *lazy* template implementations. Note that regularly
-
# registered template implementations *always* have preference over lazily
-
# registered template implementations. You should use {#register} if you
-
# depend on a specific template implementation and {#register_lazy} if there
-
# are multiple alternatives.
-
#
-
# mapping = Tilt::Mapping.new
-
# mapping.register_lazy('RDiscount::Template', 'rdiscount/template', 'md')
-
# mapping['index.md']
-
# # => RDiscount::Template
-
#
-
# {#register_lazy} takes a class name, a filename, and a list of file
-
# extensions. When you try to lookup a template name that matches the
-
# file extension, Tilt will automatically try to require the filename and
-
# constantize the class name.
-
#
-
# Unlike {#register}, there can be multiple template implementations
-
# registered lazily to the same file extension. Tilt will attempt to load the
-
# template implementations in order (registered *last* would be tried first),
-
# returning the first which doesn't raise LoadError.
-
#
-
# If all of the registered template implementations fails, Tilt will raise
-
# the exception of the first, since that was the most preferred one.
-
#
-
# mapping = Tilt::Mapping.new
-
# mapping.register_lazy('Bluecloth::Template', 'bluecloth/template', 'md')
-
# mapping.register_lazy('RDiscount::Template', 'rdiscount/template', 'md')
-
# mapping['index.md']
-
# # => RDiscount::Template
-
#
-
# In the previous example we say that RDiscount has a *higher priority* than
-
# BlueCloth. Tilt will first try to `require "rdiscount/template"`, falling
-
# back to `require "bluecloth/template"`. If none of these are successful,
-
# the first error will be raised.
-
1
class Mapping
-
# @private
-
1
attr_reader :lazy_map, :template_map
-
-
1
def initialize
-
1
@template_map = Hash.new
-
50
@lazy_map = Hash.new { |h, k| h[k] = [] }
-
end
-
-
# @private
-
1
def initialize_copy(other)
-
@template_map = other.template_map.dup
-
@lazy_map = other.lazy_map.dup
-
end
-
-
# Registers a lazy template implementation by file extension. You
-
# can have multiple lazy template implementations defined on the
-
# same file extension, in which case the template implementation
-
# defined *last* will be attempted loaded *first*.
-
#
-
# @param class_name [String] Class name of a template class.
-
# @param file [String] Filename where the template class is defined.
-
# @param extensions [Array<String>] List of extensions.
-
# @return [void]
-
#
-
# @example
-
# mapping.register_lazy 'MyEngine::Template', 'my_engine/template', 'mt'
-
#
-
# defined?(MyEngine::Template) # => false
-
# mapping['index.mt'] # => MyEngine::Template
-
# defined?(MyEngine::Template) # => true
-
1
def register_lazy(class_name, file, *extensions)
-
# Internal API
-
42
if class_name.is_a?(Symbol)
-
37
Tilt.autoload class_name, file
-
37
class_name = "Tilt::#{class_name}"
-
end
-
-
42
extensions.each do |ext|
-
72
@lazy_map[ext].unshift([class_name, file])
-
end
-
end
-
-
# Registers a template implementation by file extension. There can only be
-
# one template implementation per file extension, and this method will
-
# override any existing mapping.
-
#
-
# @param template_class
-
# @param extensions [Array<String>] List of extensions.
-
# @return [void]
-
#
-
# @example
-
# mapping.register MyEngine::Template, 'mt'
-
# mapping['index.mt'] # => MyEngine::Template
-
1
def register(template_class, *extensions)
-
if template_class.respond_to?(:to_str)
-
# Support register(ext, template_class) too
-
extensions, template_class = [template_class], extensions[0]
-
end
-
-
extensions.each do |ext|
-
@template_map[ext.to_s] = template_class
-
end
-
end
-
-
# Checks if a file extension is registered (either eagerly or
-
# lazily) in this mapping.
-
#
-
# @param ext [String] File extension.
-
#
-
# @example
-
# mapping.registered?('erb') # => true
-
# mapping.registered?('nope') # => false
-
1
def registered?(ext)
-
@template_map.has_key?(ext.downcase) or lazy?(ext)
-
end
-
-
# Instantiates a new template class based on the file.
-
#
-
# @raise [RuntimeError] if there is no template class registered for the
-
# file name.
-
#
-
# @example
-
# mapping.new('index.mt') # => instance of MyEngine::Template
-
#
-
# @see Tilt::Template.new
-
1
def new(file, line=nil, options={}, &block)
-
if template_class = self[file]
-
template_class.new(file, line, options, &block)
-
else
-
fail "No template engine registered for #{File.basename(file)}"
-
end
-
end
-
-
# Looks up a template class based on file name and/or extension.
-
#
-
# @example
-
# mapping['views/hello.erb'] # => Tilt::ERBTemplate
-
# mapping['hello.erb'] # => Tilt::ERBTemplate
-
# mapping['erb'] # => Tilt::ERBTemplate
-
#
-
# @return [template class]
-
1
def [](file)
-
_, ext = split(file)
-
ext && lookup(ext)
-
end
-
-
1
alias template_for []
-
-
# Looks up a list of template classes based on file name. If the file name
-
# has multiple extensions, it will return all template classes matching the
-
# extensions from the end.
-
#
-
# @example
-
# mapping.templates_for('views/index.haml.erb')
-
# # => [Tilt::ERBTemplate, Tilt::HamlTemplate]
-
#
-
# @return [Array<template class>]
-
1
def templates_for(file)
-
templates = []
-
-
while true
-
prefix, ext = split(file)
-
break unless ext
-
templates << lookup(ext)
-
file = prefix
-
end
-
-
templates
-
end
-
-
# Finds the extensions the template class has been registered under.
-
# @param [template class] template_class
-
1
def extensions_for(template_class)
-
res = []
-
template_map.each do |ext, klass|
-
res << ext if template_class == klass
-
end
-
lazy_map.each do |ext, choices|
-
res << ext if choices.any? { |klass, file| template_class.to_s == klass }
-
end
-
res
-
end
-
-
1
private
-
-
1
def lazy?(ext)
-
ext = ext.downcase
-
@lazy_map.has_key?(ext) && !@lazy_map[ext].empty?
-
end
-
-
1
def split(file)
-
pattern = file.to_s.downcase
-
full_pattern = pattern.dup
-
-
until registered?(pattern)
-
return if pattern.empty?
-
pattern = File.basename(pattern)
-
pattern.sub!(/^[^.]*\.?/, '')
-
end
-
-
prefix_size = full_pattern.size - pattern.size
-
[full_pattern[0,prefix_size-1], pattern]
-
end
-
-
1
def lookup(ext)
-
@template_map[ext] || lazy_load(ext)
-
end
-
-
1
LOCK = Monitor.new
-
-
1
def lazy_load(pattern)
-
return unless @lazy_map.has_key?(pattern)
-
-
LOCK.enter
-
entered = true
-
-
choices = @lazy_map[pattern]
-
-
# Check if a template class is already present
-
choices.each do |class_name, file|
-
template_class = constant_defined?(class_name)
-
if template_class
-
register(template_class, pattern)
-
return template_class
-
end
-
end
-
-
first_failure = nil
-
-
# Load in order
-
choices.each do |class_name, file|
-
begin
-
require file
-
# It's safe to eval() here because constant_defined? will
-
# raise NameError on invalid constant names
-
template_class = eval(class_name)
-
rescue LoadError => ex
-
first_failure ||= ex
-
else
-
register(template_class, pattern)
-
return template_class
-
end
-
end
-
-
raise first_failure if first_failure
-
ensure
-
LOCK.exit if entered
-
end
-
-
# This is due to a bug in JRuby (see GH issue jruby/jruby#3585)
-
1
Tilt.autoload :Dummy, "tilt/dummy"
-
1
require "tilt/dummy"
-
1
AUTOLOAD_IS_BROKEN = Tilt.autoload?(:Dummy)
-
-
# The proper behavior (in MRI) for autoload? is to
-
# return `false` when the constant/file has been
-
# explicitly required.
-
#
-
# However, in JRuby it returns `true` even after it's
-
# been required. In that case it turns out that `defined?`
-
# returns `"constant"` if it exists and `nil` when it doesn't.
-
# This is actually a second bug: `defined?` should resolve
-
# autoload (aka. actually try to require the file).
-
#
-
# We use the second bug in order to resolve the first bug.
-
-
1
def constant_defined?(name)
-
name.split('::').inject(Object) do |scope, n|
-
if scope.autoload?(n)
-
if !AUTOLOAD_IS_BROKEN
-
return false
-
end
-
-
if eval("!defined?(scope::#{n})")
-
return false
-
end
-
end
-
return false if !scope.const_defined?(n)
-
scope.const_get(n)
-
end
-
end
-
end
-
end
-
1
require 'thread'
-
-
1
module Tilt
-
# @private
-
1
TOPOBJECT = if RUBY_VERSION >= '2.0'
-
# @private
-
1
module CompiledTemplates
-
1
self
-
end
-
elsif RUBY_VERSION >= '1.9'
-
BasicObject
-
else
-
Object
-
end
-
# @private
-
1
LOCK = Mutex.new
-
-
# Base class for template implementations. Subclasses must implement
-
# the #prepare method and one of the #evaluate or #precompiled_template
-
# methods.
-
1
class Template
-
# Template source; loaded from a file or given directly.
-
1
attr_reader :data
-
-
# The name of the file where the template data was loaded from.
-
1
attr_reader :file
-
-
# The line number in #file where template data was loaded from.
-
1
attr_reader :line
-
-
# A Hash of template engine specific options. This is passed directly
-
# to the underlying engine and is not used by the generic template
-
# interface.
-
1
attr_reader :options
-
-
1
class << self
-
# An empty Hash that the template engine can populate with various
-
# metadata.
-
1
def metadata
-
@metadata ||= {}
-
end
-
-
# @deprecated Use `.metadata[:mime_type]` instead.
-
1
def default_mime_type
-
metadata[:mime_type]
-
end
-
-
# @deprecated Use `.metadata[:mime_type] = val` instead.
-
1
def default_mime_type=(value)
-
metadata[:mime_type] = value
-
end
-
end
-
-
# Create a new template with the file, line, and options specified. By
-
# default, template data is read from the file. When a block is given,
-
# it should read template data and return as a String. When file is nil,
-
# a block is required.
-
#
-
# All arguments are optional.
-
1
def initialize(file=nil, line=1, options={}, &block)
-
@file, @line, @options = nil, 1, {}
-
-
[options, line, file].compact.each do |arg|
-
case
-
when arg.respond_to?(:to_str) ; @file = arg.to_str
-
when arg.respond_to?(:to_int) ; @line = arg.to_int
-
when arg.respond_to?(:to_hash) ; @options = arg.to_hash.dup
-
when arg.respond_to?(:path) ; @file = arg.path
-
when arg.respond_to?(:to_path) ; @file = arg.to_path
-
else raise TypeError, "Can't load the template file. Pass a string with a path " +
-
"or an object that responds to 'to_str', 'path' or 'to_path'"
-
end
-
end
-
-
raise ArgumentError, "file or block required" if (@file || block).nil?
-
-
# used to hold compiled template methods
-
@compiled_method = {}
-
-
# used on 1.9 to set the encoding if it is not set elsewhere (like a magic comment)
-
# currently only used if template compiles to ruby
-
@default_encoding = @options.delete :default_encoding
-
-
# load template data and prepare (uses binread to avoid encoding issues)
-
@reader = block || lambda { |t| read_template_file }
-
@data = @reader.call(self)
-
-
if @data.respond_to?(:force_encoding)
-
if default_encoding
-
@data = @data.dup if @data.frozen?
-
@data.force_encoding(default_encoding)
-
end
-
-
if !@data.valid_encoding?
-
raise Encoding::InvalidByteSequenceError, "#{eval_file} is not valid #{@data.encoding}"
-
end
-
end
-
-
prepare
-
end
-
-
# Render the template in the given scope with the locals specified. If a
-
# block is given, it is typically available within the template via
-
# +yield+.
-
1
def render(scope=nil, locals={}, &block)
-
scope ||= Object.new
-
current_template = Thread.current[:tilt_current_template]
-
Thread.current[:tilt_current_template] = self
-
evaluate(scope, locals || {}, &block)
-
ensure
-
Thread.current[:tilt_current_template] = current_template
-
end
-
-
# The basename of the template file.
-
1
def basename(suffix='')
-
File.basename(file, suffix) if file
-
end
-
-
# The template file's basename with all extensions chomped off.
-
1
def name
-
basename.split('.', 2).first if basename
-
end
-
-
# The filename used in backtraces to describe the template.
-
1
def eval_file
-
file || '(__TEMPLATE__)'
-
end
-
-
# An empty Hash that the template engine can populate with various
-
# metadata.
-
1
def metadata
-
if respond_to?(:allows_script?)
-
self.class.metadata.merge(:allows_script => allows_script?)
-
else
-
self.class.metadata
-
end
-
end
-
-
1
protected
-
-
# @!group For template implementations
-
-
# The encoding of the source data. Defaults to the
-
# default_encoding-option if present. You may override this method
-
# in your template class if you have a better hint of the data's
-
# encoding.
-
1
def default_encoding
-
@default_encoding
-
end
-
-
# Do whatever preparation is necessary to setup the underlying template
-
# engine. Called immediately after template data is loaded. Instance
-
# variables set in this method are available when #evaluate is called.
-
#
-
# Subclasses must provide an implementation of this method.
-
1
def prepare
-
raise NotImplementedError
-
end
-
-
# Execute the compiled template and return the result string. Template
-
# evaluation is guaranteed to be performed in the scope object with the
-
# locals specified and with support for yielding to the block.
-
#
-
# This method is only used by source generating templates. Subclasses that
-
# override render() may not support all features.
-
1
def evaluate(scope, locals, &block)
-
locals_keys = locals.keys
-
locals_keys.sort!{|x, y| x.to_s <=> y.to_s}
-
method = compiled_method(locals_keys, scope.is_a?(Module) ? scope : scope.class)
-
method.bind(scope).call(locals, &block)
-
end
-
-
# Generates all template source by combining the preamble, template, and
-
# postamble and returns a two-tuple of the form: [source, offset], where
-
# source is the string containing (Ruby) source code for the template and
-
# offset is the integer line offset where line reporting should begin.
-
#
-
# Template subclasses may override this method when they need complete
-
# control over source generation or want to adjust the default line
-
# offset. In most cases, overriding the #precompiled_template method is
-
# easier and more appropriate.
-
1
def precompiled(local_keys)
-
preamble = precompiled_preamble(local_keys)
-
template = precompiled_template(local_keys)
-
postamble = precompiled_postamble(local_keys)
-
source = String.new
-
-
# Ensure that our generated source code has the same encoding as the
-
# the source code generated by the template engine.
-
if source.respond_to?(:force_encoding)
-
template_encoding = extract_encoding(template)
-
-
source.force_encoding(template_encoding)
-
template.force_encoding(template_encoding)
-
end
-
-
source << preamble << "\n" << template << "\n" << postamble
-
-
[source, preamble.count("\n")+1]
-
end
-
-
# A string containing the (Ruby) source code for the template. The
-
# default Template#evaluate implementation requires either this
-
# method or the #precompiled method be overridden. When defined,
-
# the base Template guarantees correct file/line handling, locals
-
# support, custom scopes, proper encoding, and support for template
-
# compilation.
-
1
def precompiled_template(local_keys)
-
raise NotImplementedError
-
end
-
-
1
def precompiled_preamble(local_keys)
-
''
-
end
-
-
1
def precompiled_postamble(local_keys)
-
''
-
end
-
-
# !@endgroup
-
-
1
private
-
-
1
def read_template_file
-
data = File.open(file, 'rb') { |io| io.read }
-
if data.respond_to?(:force_encoding)
-
# Set it to the default external (without verifying)
-
data.force_encoding(Encoding.default_external) if Encoding.default_external
-
end
-
data
-
end
-
-
# The compiled method for the locals keys provided.
-
1
def compiled_method(locals_keys, scope_class=nil)
-
LOCK.synchronize do
-
@compiled_method[[scope_class, locals_keys]] ||= compile_template_method(locals_keys, scope_class)
-
end
-
end
-
-
1
def local_extraction(local_keys)
-
local_keys.map do |k|
-
if k.to_s =~ /\A[a-z_][a-zA-Z_0-9]*\z/
-
"#{k} = locals[#{k.inspect}]"
-
else
-
raise "invalid locals key: #{k.inspect} (keys must be variable names)"
-
end
-
end.join("\n")
-
end
-
-
1
def compile_template_method(local_keys, scope_class=nil)
-
source, offset = precompiled(local_keys)
-
local_code = local_extraction(local_keys)
-
-
method_name = "__tilt_#{Thread.current.object_id.abs}"
-
method_source = String.new
-
-
if method_source.respond_to?(:force_encoding)
-
method_source.force_encoding(source.encoding)
-
end
-
-
method_source << <<-RUBY
-
TOPOBJECT.class_eval do
-
def #{method_name}(locals)
-
#{local_code}
-
RUBY
-
offset += method_source.count("\n")
-
method_source << source
-
method_source << "\nend;end;"
-
(scope_class || Object).class_eval(method_source, eval_file, line - offset)
-
unbind_compiled_method(method_name)
-
end
-
-
1
def unbind_compiled_method(method_name)
-
method = TOPOBJECT.instance_method(method_name)
-
TOPOBJECT.class_eval { remove_method(method_name) }
-
method
-
end
-
-
1
def extract_encoding(script)
-
extract_magic_comment(script) || script.encoding
-
end
-
-
1
def extract_magic_comment(script)
-
binary(script) do
-
script[/\A[ \t]*\#.*coding\s*[=:]\s*([[:alnum:]\-_]+).*$/n, 1]
-
end
-
end
-
-
1
def binary(string)
-
original_encoding = string.encoding
-
string.force_encoding(Encoding::BINARY)
-
yield
-
ensure
-
string.force_encoding(original_encoding)
-
end
-
end
-
end
-
# encoding: UTF-8
-
-
1
require "json"
-
1
require "base64"
-
1
require "execjs"
-
1
require "uglifier/version"
-
-
# A wrapper around the UglifyJS interface
-
1
class Uglifier
-
# Error class for compilation errors.
-
1
class Error < StandardError; end
-
-
# UglifyJS source path
-
1
SourcePath = File.expand_path("../uglify.js", __FILE__)
-
# UglifyJS with Harmony source path
-
1
HarmonySourcePath = File.expand_path("../uglify-harmony.js", __FILE__)
-
# Source Map path
-
1
SourceMapPath = File.expand_path("../source-map.js", __FILE__)
-
# ES5 shims source path
-
1
ES5FallbackPath = File.expand_path("../es5.js", __FILE__)
-
# String.split shim source path
-
1
SplitFallbackPath = File.expand_path("../split.js", __FILE__)
-
# UglifyJS wrapper path
-
1
UglifyJSWrapperPath = File.expand_path("../uglifier.js", __FILE__)
-
-
# Default options for compilation
-
DEFAULTS = {
-
# rubocop:disable LineLength
-
1
:output => {
-
:ascii_only => true, # Escape non-ASCII characterss
-
:comments => :copyright, # Preserve comments (:all, :jsdoc, :copyright, :none)
-
:inline_script => false, # Escape occurrences of </script in strings
-
:quote_keys => false, # Quote keys in object literals
-
:max_line_len => 32 * 1024, # Maximum line length in minified code
-
:bracketize => false, # Bracketize if, for, do, while or with statements, even if their body is a single statement
-
:semicolons => true, # Separate statements with semicolons
-
:preserve_line => false, # Preserve line numbers in outputs
-
:beautify => false, # Beautify output
-
:indent_level => 4, # Indent level in spaces
-
:indent_start => 0, # Starting indent level
-
:width => 80, # Specify line width when beautifier is used (only with beautifier)
-
:preamble => nil, # Preamble for the generated JS file. Can be used to insert any code or comment.
-
:wrap_iife => false, # Wrap IIFEs in parenthesis. Note: this disables the negate_iife compression option.
-
:shebang => true, # Preserve shebang (#!) in preamble (shell scripts)
-
:quote_style => 0, # Quote style, possible values :auto (default), :single, :double, :original
-
:keep_quoted_props => false # Keep quotes property names
-
},
-
:mangle => {
-
:eval => false, # Mangle names when eval of when is used in scope
-
:reserved => ["$super"], # Argument names to be excluded from mangling
-
:properties => false, # Mangle property names
-
:toplevel => false, # Mangle names declared in the toplevel scope
-
}, # Mangle variable and function names, set to false to skip mangling
-
:compress => {
-
:sequences => true, # Allow statements to be joined by commas
-
:properties => true, # Rewrite property access using the dot notation
-
:dead_code => true, # Remove unreachable code
-
:drop_debugger => true, # Remove debugger; statements
-
:unsafe => false, # Apply "unsafe" transformations
-
:unsafe_comps => false, # Reverse < and <= to > and >= to allow improved compression. This might be unsafe when an at least one of two operands is an object with computed values due the use of methods like get, or valueOf. This could cause change in execution order after operands in the comparison are switching. Compression only works if both comparisons and unsafe_comps are both set to true.
-
:unsafe_math => false, # Optimize numerical expressions like 2 * x * 3 into 6 * x, which may give imprecise floating point results.
-
:unsafe_proto => false, # Optimize expressions like Array.prototype.slice.call(a) into [].slice.call(a)
-
:conditionals => true, # Optimize for if-s and conditional expressions
-
:comparisons => true, # Apply binary node optimizations for comparisons
-
:evaluate => true, # Attempt to evaluate constant expressions
-
:booleans => true, # Various optimizations to boolean contexts
-
:loops => true, # Optimize loops when condition can be statically determined
-
:unused => true, # Drop unreferenced functions and variables (simple direct variable assignments do not count as references unless set to `"keep_assign"`)
-
:toplevel => false, # Drop unreferenced top-level functions and variables
-
:top_retain => [], # prevent specific toplevel functions and variables from `unused` removal
-
:hoist_funs => true, # Hoist function declarations
-
:hoist_vars => false, # Hoist var declarations
-
:if_return => true, # Optimizations for if/return and if/continue
-
:join_vars => true, # Join consecutive var statements
-
:collapse_vars => true, # Collapse single-use var and const definitions when possible.
-
:reduce_funcs => false, # Inline single-use functions as function expressions. Depends on reduce_vars.
-
:reduce_vars => false, # Collapse variables assigned with and used as constant values.
-
:negate_iife => true, # Negate immediately invoked function expressions to avoid extra parens
-
:pure_getters => false, # Assume that object property access does not have any side-effects
-
:pure_funcs => nil, # List of functions without side-effects. Can safely discard function calls when the result value is not used
-
:drop_console => false, # Drop calls to console.* functions
-
:keep_fargs => false, # Preserve unused function arguments
-
:keep_fnames => false, # Do not drop names in function definitions
-
:passes => 1, # Number of times to run compress. Raising the number of passes will increase compress time, but can produce slightly smaller code.
-
:keep_infinity => false, # Prevent compression of Infinity to 1/0
-
:side_effects => true, # Pass false to disable potentially dropping functions marked as "pure" using pure comment annotation. See UglifyJS documentation for details.
-
:switches => true, # de-duplicate and remove unreachable switch branches
-
}, # Apply transformations to code, set to false to skip
-
:parse => {
-
:bare_returns => false, # Allow top-level return statements.
-
:expression => false, # Parse a single expression, rather than a program (for parsing JSON).
-
:html5_comments => true, # Ignore HTML5 comments in input
-
:shebang => true, # support #!command as the first line
-
:strict => false
-
},
-
:define => {}, # Define values for symbol replacement
-
:keep_fnames => false, # Generate code safe for the poor souls relying on Function.prototype.name at run-time. Sets both compress and mangle keep_fanems to true.
-
:toplevel => false,
-
:ie8 => true, # Generate safe code for IE8
-
:source_map => false, # Generate source map
-
:error_context_lines => 8, # How many lines surrounding the error line
-
:harmony => false # Enable ES6/Harmony mode (experimental). Disabling mangling and compressing is recommended with Harmony mode.
-
}
-
-
1
EXTRA_OPTIONS = [:comments, :mangle_properties]
-
-
MANGLE_PROPERTIES_DEFAULTS = {
-
1
:debug => false, # Add debug prefix and suffix to mangled properties
-
:regex => nil, # A regular expression to filter property names to be mangled
-
:keep_quoted => false, # Keep quoted property names
-
:reserved => [], # List of properties that should not be mangled
-
:builtins => false, # Mangle properties that overlap with standard JS globals
-
}
-
-
1
SOURCE_MAP_DEFAULTS = {
-
:map_url => false, # Url for source mapping to be appended in minified source
-
:url => false, # Url for original source to be appended in minified source
-
:sources_content => false, # Include original source content in map
-
:filename => nil, # The filename of the input file
-
:root => nil, # The URL of the directory which contains :filename
-
:output_filename => nil, # The filename or URL where the minified output can be found
-
:input_source_map => nil # The contents of the source map describing the input
-
}
-
-
# rubocop:enable LineLength
-
-
# Minifies JavaScript code using implicit context.
-
#
-
# @param source [IO, String] valid JS source code.
-
# @param options [Hash] optional overrides to +Uglifier::DEFAULTS+
-
# @return [String] minified code.
-
1
def self.compile(source, options = {})
-
new(options).compile(source)
-
end
-
-
# Minifies JavaScript code and generates a source map using implicit context.
-
#
-
# @param source [IO, String] valid JS source code.
-
# @param options [Hash] optional overrides to +Uglifier::DEFAULTS+
-
# @return [Array(String, String)] minified code and source map.
-
1
def self.compile_with_map(source, options = {})
-
new(options).compile_with_map(source)
-
end
-
-
# Initialize new context for Uglifier with given options
-
#
-
# @param options [Hash] optional overrides to +Uglifier::DEFAULTS+
-
1
def initialize(options = {})
-
1
(options.keys - DEFAULTS.keys - EXTRA_OPTIONS)[0..1].each do |missing|
-
raise ArgumentError, "Invalid option: #{missing}"
-
end
-
1
@options = options
-
end
-
-
# Minifies JavaScript code
-
#
-
# @param source [IO, String] valid JS source code.
-
# @return [String] minified code.
-
1
def compile(source)
-
1
if @options[:source_map]
-
compiled, source_map = run_uglifyjs(source, true)
-
source_map_uri = Base64.strict_encode64(source_map)
-
source_map_mime = "application/json;charset=utf-8;base64"
-
compiled + "\n//# sourceMappingURL=data:#{source_map_mime},#{source_map_uri}"
-
else
-
1
run_uglifyjs(source, false)
-
end
-
end
-
1
alias_method :compress, :compile
-
-
# Minifies JavaScript code and generates a source map
-
#
-
# @param source [IO, String] valid JS source code.
-
# @return [Array(String, String)] minified code and source map.
-
1
def compile_with_map(source)
-
run_uglifyjs(source, true)
-
end
-
-
1
private
-
-
1
def context
-
1
@context ||= begin
-
1
source = harmony? ? source_with(HarmonySourcePath) : source_with(SourcePath)
-
1
ExecJS.compile(source)
-
end
-
end
-
-
1
def source_map_comments
-
1
return '' unless @options[:source_map].respond_to?(:[])
-
-
suffix = ''
-
if @options[:source_map][:map_url]
-
suffix += "\n//# sourceMappingURL=" + @options[:source_map][:map_url]
-
end
-
-
suffix += "\n//# sourceURL=" + @options[:source_map][:url] if @options[:source_map][:url]
-
suffix
-
end
-
-
1
def source_with(path)
-
1
[ES5FallbackPath, SplitFallbackPath, SourceMapPath, path,
-
UglifyJSWrapperPath].map do |file|
-
5
File.open(file, "r:UTF-8", &:read)
-
end.join("\n")
-
end
-
-
# Run UglifyJS for given source code
-
1
def run_uglifyjs(input, generate_map)
-
1
source = read_source(input)
-
1
input_map = input_source_map(source, generate_map)
-
options = {
-
1
:source => source,
-
:output => output_options,
-
:compress => compressor_options,
-
:mangle => mangle_options,
-
:parse => parse_options,
-
:sourceMap => source_map_options(input_map),
-
:ie8 => ie8?
-
}
-
-
1
parse_result(context.call("uglifier", options), generate_map, options)
-
end
-
-
1
def harmony?
-
3
@options[:harmony]
-
end
-
-
1
def harmony_error_message(message)
-
if message.start_with?("Unexpected token")
-
". To use ES6 syntax, harmony mode must be enabled with " \
-
"Uglifier.new(:harmony => true)."
-
else
-
""
-
end
-
end
-
-
1
def error_context_lines
-
@options.fetch(:error_context_lines, DEFAULTS[:error_context_lines]).to_i
-
end
-
-
1
def error_context_format_options(low, high, line_index, column)
-
line_width = high.to_s.size
-
{
-
:line_index => line_index,
-
:base_index => low,
-
:line_width => line_width,
-
:line_format => "\e[36m%#{line_width + 1}d\e[0m ", # cyan
-
:col => column
-
}
-
end
-
-
1
def format_error_line(line, options)
-
# light red
-
indicator = ' => '.rjust(options[:line_width] + 2)
-
colored_line = "#{line[0...options[:col]]}\e[91m#{line[options[:col]..-1]}"
-
"\e[91m#{indicator}\e[0m#{colored_line}\e[0m"
-
end
-
-
1
def format_lines(lines, options)
-
lines.map.with_index do |line, index|
-
if options[:base_index] + index == options[:line_index]
-
format_error_line(line, options)
-
else
-
"#{options[:line_format] % (options[:base_index] + index + 1)}#{line}"
-
end
-
end
-
end
-
-
1
def context_lines_message(source, line_number, column)
-
return if line_number.nil?
-
-
line_index = line_number - 1
-
lines = source.split("\n")
-
-
first_line = [line_index - error_context_lines, 0].max
-
last_line = [line_number + error_context_lines, lines.size].min
-
options = error_context_format_options(first_line, last_line, line_index, column)
-
context_lines = lines[first_line...last_line]
-
-
"--\n#{format_lines(context_lines, options).join("\n")}\n=="
-
end
-
-
1
def error_message(result, options)
-
err = result['error']
-
harmony_msg = harmony? ? '' : harmony_error_message(err['message'].to_s)
-
src_ctx = context_lines_message(options[:source], err['line'], err['col'])
-
"#{err['message']}#{harmony_msg}\n#{src_ctx}"
-
end
-
-
1
def parse_result(result, generate_map, options)
-
1
raise Error, error_message(result, options) if result.has_key?('error')
-
-
1
if generate_map
-
[result['code'] + source_map_comments, result['map']]
-
else
-
1
result['code'] + source_map_comments
-
end
-
end
-
-
1
def read_source(source)
-
1
if source.respond_to?(:read)
-
source.read
-
else
-
1
source.to_s
-
end
-
end
-
-
1
def mangle_options
-
1
defaults = conditional_option(
-
DEFAULTS[:mangle],
-
:keep_fnames => keep_fnames?(:mangle)
-
)
-
-
1
conditional_option(
-
@options[:mangle],
-
defaults,
-
:properties => mangle_properties_options
-
)
-
end
-
-
1
def mangle_properties_options
-
1
mangle_options = conditional_option(@options[:mangle], DEFAULTS[:mangle])
-
-
mangle_properties_options =
-
1
if @options.has_key?(:mangle_properties)
-
@options[:mangle_properties]
-
else
-
1
mangle_options && mangle_options[:properties]
-
end
-
-
1
options = conditional_option(mangle_properties_options, MANGLE_PROPERTIES_DEFAULTS)
-
-
1
if options && options[:regex]
-
options.merge(:regex => encode_regexp(options[:regex]))
-
else
-
1
options
-
end
-
end
-
-
1
def compressor_options
-
1
defaults = conditional_option(
-
DEFAULTS[:compress],
-
:global_defs => @options[:define] || {}
-
)
-
-
1
conditional_option(
-
@options[:compress],
-
defaults,
-
{ :keep_fnames => keep_fnames?(:compress) }.merge(negate_iife_block)
-
)
-
end
-
-
# Prevent negate_iife when wrap_iife is true
-
1
def negate_iife_block
-
1
if output_options[:wrap_iife]
-
{ :negate_iife => false }
-
else
-
1
{}
-
end
-
end
-
-
1
def comment_options
-
2
case comment_setting
-
when :all, true
-
true
-
when :jsdoc
-
"jsdoc"
-
when :copyright
-
2
encode_regexp(/(^!)|Copyright/i)
-
when Regexp
-
encode_regexp(comment_setting)
-
else
-
false
-
end
-
end
-
-
1
def quote_style
-
2
option = conditional_option(@options[:output], DEFAULTS[:output])[:quote_style]
-
2
case option
-
when :single
-
1
-
when :double
-
2
-
when :original
-
3
-
when Numeric
-
2
option
-
else # auto
-
0
-
end
-
end
-
-
1
def comment_setting
-
2
if @options.has_key?(:output) && @options[:output].has_key?(:comments)
-
@options[:output][:comments]
-
2
elsif @options.has_key?(:comments)
-
@options[:comments]
-
else
-
2
DEFAULTS[:output][:comments]
-
end
-
end
-
-
1
def output_options
-
2
migrate_braces(DEFAULTS[:output].merge(@options[:output] || {}))
-
.merge(:comments => comment_options, :quote_style => quote_style)
-
end
-
-
1
def migrate_braces(options)
-
2
if harmony?
-
2
options
-
else
-
options.merge(:braces => options[:bracketize]).delete_if { |key| key == :bracketize }
-
end
-
end
-
-
1
def ie8?
-
1
@options.fetch(:ie8, DEFAULTS[:ie8])
-
end
-
-
1
def keep_fnames?(type)
-
2
if @options[:keep_fnames] || DEFAULTS[:keep_fnames]
-
true
-
else
-
2
@options[type].respond_to?(:[]) && @options[type][:keep_fnames] ||
-
DEFAULTS[type].respond_to?(:[]) && DEFAULTS[type][:keep_fnames]
-
end
-
end
-
-
1
def source_map_options(input_map)
-
1
options = conditional_option(@options[:source_map], SOURCE_MAP_DEFAULTS) || SOURCE_MAP_DEFAULTS
-
-
{
-
1
:input => options[:filename],
-
:filename => options[:output_filename],
-
:root => options.fetch(:root) { input_map ? input_map["sourceRoot"] : nil },
-
:content => input_map,
-
#:map_url => options[:map_url],
-
:url => options[:url],
-
:includeSources => options[:sources_content]
-
}
-
end
-
-
1
def parse_options
-
1
conditional_option(@options[:parse], DEFAULTS[:parse])
-
.merge(parse_source_map_options)
-
end
-
-
1
def parse_source_map_options
-
1
if @options[:source_map].respond_to?(:[])
-
{ :filename => @options[:source_map][:filename] }
-
else
-
1
{}
-
end
-
end
-
-
1
def enclose_options
-
if @options[:enclose]
-
@options[:enclose].map do |pair|
-
pair.first + ':' + pair.last
-
end
-
else
-
false
-
end
-
end
-
-
1
def encode_regexp(regexp)
-
2
modifiers = if regexp.casefold?
-
2
"i"
-
else
-
""
-
end
-
-
2
[regexp.source, modifiers]
-
end
-
-
1
def conditional_option(value, defaults, overrides = {})
-
10
if value == true || value.nil?
-
7
defaults.merge(overrides)
-
3
elsif value
-
2
defaults.merge(value).merge(overrides)
-
else
-
1
false
-
end
-
end
-
-
1
def sanitize_map_root(map)
-
if map.nil?
-
nil
-
elsif map.is_a? String
-
sanitize_map_root(JSON.parse(map))
-
elsif map["sourceRoot"] == ""
-
map.merge("sourceRoot" => nil)
-
else
-
map
-
end
-
end
-
-
1
def extract_source_mapping_url(source)
-
comment_start = %r{(?://|/\*\s*)}
-
comment_end = %r{\s*(?:\r?\n?\*/|$)?}
-
source_mapping_regex = /#{comment_start}[@#]\ssourceMappingURL=\s*(\S*?)#{comment_end}/
-
rest = /\s#{comment_start}[@#]\s[a-zA-Z]+=\s*(?:\S*?)#{comment_end}/
-
regex = /#{source_mapping_regex}(?:#{rest})*\Z/m
-
match = regex.match(source)
-
match && match[1]
-
end
-
-
1
def input_source_map(source, generate_map)
-
1
return nil unless generate_map
-
source_map_options = @options[:source_map].is_a?(Hash) ? @options[:source_map] : {}
-
sanitize_map_root(source_map_options.fetch(:input_source_map) do
-
url = extract_source_mapping_url(source)
-
Base64.strict_decode64(url.split(",", 2)[-1]) if url && url.start_with?("data:")
-
end)
-
rescue ArgumentError, JSON::ParserError
-
nil
-
end
-
end
-
1
class Uglifier
-
# Current version of Uglifier.
-
1
VERSION = "4.2.0"
-
end
-
# frozen_string_literal: true
-
#
-
# = base64.rb: methods for base64-encoding and -decoding strings
-
#
-
-
# The Base64 module provides for the encoding (#encode64, #strict_encode64,
-
# #urlsafe_encode64) and decoding (#decode64, #strict_decode64,
-
# #urlsafe_decode64) of binary data using a Base64 representation.
-
#
-
# == Example
-
#
-
# A simple encoding and decoding.
-
#
-
# require "base64"
-
#
-
# enc = Base64.encode64('Send reinforcements')
-
# # -> "U2VuZCByZWluZm9yY2VtZW50cw==\n"
-
# plain = Base64.decode64(enc)
-
# # -> "Send reinforcements"
-
#
-
# The purpose of using base64 to encode data is that it translates any
-
# binary data into purely printable characters.
-
-
1
module Base64
-
1
module_function
-
-
# Returns the Base64-encoded version of +bin+.
-
# This method complies with RFC 2045.
-
# Line feeds are added to every 60 encoded characters.
-
#
-
# require 'base64'
-
# Base64.encode64("Now is the time for all good coders\nto learn Ruby")
-
#
-
# <i>Generates:</i>
-
#
-
# Tm93IGlzIHRoZSB0aW1lIGZvciBhbGwgZ29vZCBjb2RlcnMKdG8gbGVhcm4g
-
# UnVieQ==
-
1
def encode64(bin)
-
[bin].pack("m")
-
end
-
-
# Returns the Base64-decoded version of +str+.
-
# This method complies with RFC 2045.
-
# Characters outside the base alphabet are ignored.
-
#
-
# require 'base64'
-
# str = 'VGhpcyBpcyBsaW5lIG9uZQpUaGlzIG' +
-
# 'lzIGxpbmUgdHdvClRoaXMgaXMgbGlu' +
-
# 'ZSB0aHJlZQpBbmQgc28gb24uLi4K'
-
# puts Base64.decode64(str)
-
#
-
# <i>Generates:</i>
-
#
-
# This is line one
-
# This is line two
-
# This is line three
-
# And so on...
-
1
def decode64(str)
-
str.unpack1("m")
-
end
-
-
# Returns the Base64-encoded version of +bin+.
-
# This method complies with RFC 4648.
-
# No line feeds are added.
-
1
def strict_encode64(bin)
-
[bin].pack("m0")
-
end
-
-
# Returns the Base64-decoded version of +str+.
-
# This method complies with RFC 4648.
-
# ArgumentError is raised if +str+ is incorrectly padded or contains
-
# non-alphabet characters. Note that CR or LF are also rejected.
-
1
def strict_decode64(str)
-
str.unpack1("m0")
-
end
-
-
# Returns the Base64-encoded version of +bin+.
-
# This method complies with ``Base 64 Encoding with URL and Filename Safe
-
# Alphabet'' in RFC 4648.
-
# The alphabet uses '-' instead of '+' and '_' instead of '/'.
-
# Note that the result can still contain '='.
-
# You can remove the padding by setting +padding+ as false.
-
1
def urlsafe_encode64(bin, padding: true)
-
str = strict_encode64(bin)
-
str.tr!("+/", "-_")
-
str.delete!("=") unless padding
-
str
-
end
-
-
# Returns the Base64-decoded version of +str+.
-
# This method complies with ``Base 64 Encoding with URL and Filename Safe
-
# Alphabet'' in RFC 4648.
-
# The alphabet uses '-' instead of '+' and '_' instead of '/'.
-
#
-
# The padding character is optional.
-
# This method accepts both correctly-padded and unpadded input.
-
# Note that it still rejects incorrectly-padded input.
-
1
def urlsafe_decode64(str)
-
# NOTE: RFC 4648 does say nothing about unpadded input, but says that
-
# "the excess pad characters MAY also be ignored", so it is inferred that
-
# unpadded input is also acceptable.
-
str = str.tr("-_", "+/")
-
if !str.end_with?("=") && str.length % 4 != 0
-
str = str.ljust((str.length + 3) & ~3, "=")
-
end
-
strict_decode64(str)
-
end
-
end
-
# frozen_string_literal: true
-
# = delegate -- Support for the Delegation Pattern
-
#
-
# Documentation by James Edward Gray II and Gavin Sinclair
-
-
##
-
# This library provides three different ways to delegate method calls to an
-
# object. The easiest to use is SimpleDelegator. Pass an object to the
-
# constructor and all methods supported by the object will be delegated. This
-
# object can be changed later.
-
#
-
# Going a step further, the top level DelegateClass method allows you to easily
-
# setup delegation through class inheritance. This is considerably more
-
# flexible and thus probably the most common use for this library.
-
#
-
# Finally, if you need full control over the delegation scheme, you can inherit
-
# from the abstract class Delegator and customize as needed. (If you find
-
# yourself needing this control, have a look at Forwardable which is also in
-
# the standard library. It may suit your needs better.)
-
#
-
# SimpleDelegator's implementation serves as a nice example of the use of
-
# Delegator:
-
#
-
# class SimpleDelegator < Delegator
-
# def __getobj__
-
# @delegate_sd_obj # return object we are delegating to, required
-
# end
-
#
-
# def __setobj__(obj)
-
# @delegate_sd_obj = obj # change delegation object,
-
# # a feature we're providing
-
# end
-
# end
-
#
-
# == Notes
-
#
-
# Be advised, RDoc will not detect delegated methods.
-
#
-
1
class Delegator < BasicObject
-
1
kernel = ::Kernel.dup
-
1
kernel.class_eval do
-
1
alias __raise__ raise
-
1
[:to_s, :inspect, :=~, :!~, :===, :<=>, :hash].each do |m|
-
7
undef_method m
-
end
-
1
private_instance_methods.each do |m|
-
73
if /\Ablock_given\?\z|\Aiterator\?\z|\A__.*__\z/ =~ m
-
6
next
-
end
-
67
undef_method m
-
end
-
end
-
1
include kernel
-
-
# :stopdoc:
-
1
def self.const_missing(n)
-
117
::Object.const_get(n)
-
end
-
# :startdoc:
-
-
##
-
# :method: raise
-
# Use #__raise__ if your Delegator does not have a object to delegate the
-
# #raise method call.
-
#
-
-
#
-
# Pass in the _obj_ to delegate method calls to. All methods supported by
-
# _obj_ will be delegated to.
-
#
-
1
def initialize(obj)
-
546
__setobj__(obj)
-
end
-
-
#
-
# Handles the magic of delegation through \_\_getobj\_\_.
-
#
-
1
ruby2_keywords def method_missing(m, *args, &block)
-
32
r = true
-
32
target = self.__getobj__ {r = false}
-
-
32
if r && target_respond_to?(target, m, false)
-
32
target.__send__(m, *args, &block)
-
elsif ::Kernel.method_defined?(m) || ::Kernel.private_method_defined?(m)
-
::Kernel.instance_method(m).bind_call(self, *args, &block)
-
else
-
super(m, *args, &block)
-
end
-
end
-
-
#
-
# Checks for a method provided by this the delegate object by forwarding the
-
# call through \_\_getobj\_\_.
-
#
-
1
def respond_to_missing?(m, include_private)
-
r = true
-
target = self.__getobj__ {r = false}
-
r &&= target_respond_to?(target, m, include_private)
-
if r && include_private && !target_respond_to?(target, m, false)
-
warn "delegator does not forward private method \##{m}", uplevel: 3
-
return false
-
end
-
r
-
end
-
-
1
KERNEL_RESPOND_TO = ::Kernel.instance_method(:respond_to?)
-
1
private_constant :KERNEL_RESPOND_TO
-
-
# Handle BasicObject instances
-
1
private def target_respond_to?(target, m, include_private)
-
32
case target
-
when Object
-
32
target.respond_to?(m, include_private)
-
else
-
if KERNEL_RESPOND_TO.bind_call(target, :respond_to?)
-
target.respond_to?(m, include_private)
-
else
-
KERNEL_RESPOND_TO.bind_call(target, m, include_private)
-
end
-
end
-
end
-
-
#
-
# Returns the methods available to this delegate object as the union
-
# of this object's and \_\_getobj\_\_ methods.
-
#
-
1
def methods(all=true)
-
__getobj__.methods(all) | super
-
end
-
-
#
-
# Returns the methods available to this delegate object as the union
-
# of this object's and \_\_getobj\_\_ public methods.
-
#
-
1
def public_methods(all=true)
-
__getobj__.public_methods(all) | super
-
end
-
-
#
-
# Returns the methods available to this delegate object as the union
-
# of this object's and \_\_getobj\_\_ protected methods.
-
#
-
1
def protected_methods(all=true)
-
__getobj__.protected_methods(all) | super
-
end
-
-
# Note: no need to specialize private_methods, since they are not forwarded
-
-
#
-
# Returns true if two objects are considered of equal value.
-
#
-
1
def ==(obj)
-
return true if obj.equal?(self)
-
self.__getobj__ == obj
-
end
-
-
#
-
# Returns true if two objects are not considered of equal value.
-
#
-
1
def !=(obj)
-
return false if obj.equal?(self)
-
__getobj__ != obj
-
end
-
-
#
-
# Returns true if two objects are considered of equal value.
-
#
-
1
def eql?(obj)
-
return true if obj.equal?(self)
-
obj.eql?(__getobj__)
-
end
-
-
#
-
# Delegates ! to the \_\_getobj\_\_
-
#
-
1
def !
-
!__getobj__
-
end
-
-
#
-
# This method must be overridden by subclasses and should return the object
-
# method calls are being delegated to.
-
#
-
1
def __getobj__
-
__raise__ ::NotImplementedError, "need to define `__getobj__'"
-
end
-
-
#
-
# This method must be overridden by subclasses and change the object delegate
-
# to _obj_.
-
#
-
1
def __setobj__(obj)
-
__raise__ ::NotImplementedError, "need to define `__setobj__'"
-
end
-
-
#
-
# Serialization support for the object returned by \_\_getobj\_\_.
-
#
-
1
def marshal_dump
-
ivars = instance_variables.reject {|var| /\A@delegate_/ =~ var}
-
[
-
:__v2__,
-
ivars, ivars.map {|var| instance_variable_get(var)},
-
__getobj__
-
]
-
end
-
-
#
-
# Reinitializes delegation from a serialized object.
-
#
-
1
def marshal_load(data)
-
version, vars, values, obj = data
-
if version == :__v2__
-
vars.each_with_index {|var, i| instance_variable_set(var, values[i])}
-
__setobj__(obj)
-
else
-
__setobj__(data)
-
end
-
end
-
-
1
def initialize_clone(obj) # :nodoc:
-
self.__setobj__(obj.__getobj__.clone)
-
end
-
1
def initialize_dup(obj) # :nodoc:
-
self.__setobj__(obj.__getobj__.dup)
-
end
-
1
private :initialize_clone, :initialize_dup
-
-
##
-
# :method: freeze
-
# Freeze both the object returned by \_\_getobj\_\_ and self.
-
#
-
1
def freeze
-
__getobj__.freeze
-
super()
-
end
-
-
1
@delegator_api = self.public_instance_methods
-
1
def self.public_api # :nodoc:
-
4
@delegator_api
-
end
-
end
-
-
##
-
# A concrete implementation of Delegator, this class provides the means to
-
# delegate all supported method calls to the object passed into the constructor
-
# and even to change the object being delegated to at a later time with
-
# #__setobj__.
-
#
-
# class User
-
# def born_on
-
# Date.new(1989, 9, 10)
-
# end
-
# end
-
#
-
# class UserDecorator < SimpleDelegator
-
# def birth_year
-
# born_on.year
-
# end
-
# end
-
#
-
# decorated_user = UserDecorator.new(User.new)
-
# decorated_user.birth_year #=> 1989
-
# decorated_user.__getobj__ #=> #<User: ...>
-
#
-
# A SimpleDelegator instance can take advantage of the fact that SimpleDelegator
-
# is a subclass of +Delegator+ to call <tt>super</tt> to have methods called on
-
# the object being delegated to.
-
#
-
# class SuperArray < SimpleDelegator
-
# def [](*args)
-
# super + 1
-
# end
-
# end
-
#
-
# SuperArray.new([1])[0] #=> 2
-
#
-
# Here's a simple example that takes advantage of the fact that
-
# SimpleDelegator's delegation object can be changed at any time.
-
#
-
# class Stats
-
# def initialize
-
# @source = SimpleDelegator.new([])
-
# end
-
#
-
# def stats(records)
-
# @source.__setobj__(records)
-
#
-
# "Elements: #{@source.size}\n" +
-
# " Non-Nil: #{@source.compact.size}\n" +
-
# " Unique: #{@source.uniq.size}\n"
-
# end
-
# end
-
#
-
# s = Stats.new
-
# puts s.stats(%w{James Edward Gray II})
-
# puts
-
# puts s.stats([1, 2, 3, nil, 4, 5, 1, 2])
-
#
-
# Prints:
-
#
-
# Elements: 4
-
# Non-Nil: 4
-
# Unique: 4
-
#
-
# Elements: 8
-
# Non-Nil: 7
-
# Unique: 6
-
#
-
1
class SimpleDelegator < Delegator
-
# Returns the current object method calls are being delegated to.
-
1
def __getobj__
-
unless defined?(@delegate_sd_obj)
-
return yield if block_given?
-
__raise__ ::ArgumentError, "not delegated"
-
end
-
@delegate_sd_obj
-
end
-
-
#
-
# Changes the delegate object to _obj_.
-
#
-
# It's important to note that this does *not* cause SimpleDelegator's methods
-
# to change. Because of this, you probably only want to change delegation
-
# to objects of the same type as the original delegate.
-
#
-
# Here's an example of changing the delegation object.
-
#
-
# names = SimpleDelegator.new(%w{James Edward Gray II})
-
# puts names[1] # => Edward
-
# names.__setobj__(%w{Gavin Sinclair})
-
# puts names[1] # => Sinclair
-
#
-
1
def __setobj__(obj)
-
__raise__ ::ArgumentError, "cannot delegate to self" if self.equal?(obj)
-
@delegate_sd_obj = obj
-
end
-
end
-
-
1
def Delegator.delegating_block(mid) # :nodoc:
-
404
lambda do |*args, &block|
-
338
target = self.__getobj__
-
338
target.__send__(mid, *args, &block)
-
end.ruby2_keywords
-
end
-
-
#
-
# The primary interface to this library. Use to setup delegation when defining
-
# your class.
-
#
-
# class MyClass < DelegateClass(ClassToDelegateTo) # Step 1
-
# def initialize
-
# super(obj_of_ClassToDelegateTo) # Step 2
-
# end
-
# end
-
#
-
# or:
-
#
-
# MyClass = DelegateClass(ClassToDelegateTo) do # Step 1
-
# def initialize
-
# super(obj_of_ClassToDelegateTo) # Step 2
-
# end
-
# end
-
#
-
# Here's a sample of use from Tempfile which is really a File object with a
-
# few special rules about storage location and when the File should be
-
# deleted. That makes for an almost textbook perfect example of how to use
-
# delegation.
-
#
-
# class Tempfile < DelegateClass(File)
-
# # constant and class member data initialization...
-
#
-
# def initialize(basename, tmpdir=Dir::tmpdir)
-
# # build up file path/name in var tmpname...
-
#
-
# @tmpfile = File.open(tmpname, File::RDWR|File::CREAT|File::EXCL, 0600)
-
#
-
# # ...
-
#
-
# super(@tmpfile)
-
#
-
# # below this point, all methods of File are supported...
-
# end
-
#
-
# # ...
-
# end
-
#
-
1
def DelegateClass(superclass, &block)
-
4
klass = Class.new(Delegator)
-
4
ignores = [*::Delegator.public_api, :to_s, :inspect, :=~, :!~, :===]
-
4
protected_instance_methods = superclass.protected_instance_methods
-
4
protected_instance_methods -= ignores
-
4
public_instance_methods = superclass.public_instance_methods
-
4
public_instance_methods -= ignores
-
4
klass.module_eval do
-
4
def __getobj__ # :nodoc:
-
891
unless defined?(@delegate_dc_obj)
-
return yield if block_given?
-
__raise__ ::ArgumentError, "not delegated"
-
end
-
891
@delegate_dc_obj
-
end
-
4
def __setobj__(obj) # :nodoc:
-
546
__raise__ ::ArgumentError, "cannot delegate to self" if self.equal?(obj)
-
546
@delegate_dc_obj = obj
-
end
-
4
protected_instance_methods.each do |method|
-
define_method(method, Delegator.delegating_block(method))
-
protected method
-
end
-
4
public_instance_methods.each do |method|
-
404
define_method(method, Delegator.delegating_block(method))
-
end
-
end
-
4
klass.define_singleton_method :public_instance_methods do |all=true|
-
super(all) | superclass.public_instance_methods
-
end
-
4
klass.define_singleton_method :protected_instance_methods do |all=true|
-
super(all) | superclass.protected_instance_methods
-
end
-
4
klass.module_eval(&block) if block
-
4
return klass
-
end
-
# frozen_string_literal: false
-
#--
-
# sha2.rb - defines Digest::SHA2 class which wraps up the SHA256,
-
# SHA384, and SHA512 classes.
-
#++
-
# Copyright (c) 2006 Akinori MUSHA <knu@iDaemons.org>
-
#
-
# All rights reserved. You can redistribute and/or modify it under the same
-
# terms as Ruby.
-
#
-
# $Id$
-
-
1
require 'digest'
-
1
require 'digest/sha2.so'
-
-
1
module Digest
-
#
-
# A meta digest provider class for SHA256, SHA384 and SHA512.
-
#
-
# FIPS 180-2 describes SHA2 family of digest algorithms. It defines
-
# three algorithms:
-
# * one which works on chunks of 512 bits and returns a 256-bit
-
# digest (SHA256),
-
# * one which works on chunks of 1024 bits and returns a 384-bit
-
# digest (SHA384),
-
# * and one which works on chunks of 1024 bits and returns a 512-bit
-
# digest (SHA512).
-
#
-
# ==Examples
-
# require 'digest'
-
#
-
# # Compute a complete digest
-
# Digest::SHA2.hexdigest 'abc' # => "ba7816bf8..."
-
# Digest::SHA2.new(256).hexdigest 'abc' # => "ba7816bf8..."
-
# Digest::SHA256.hexdigest 'abc' # => "ba7816bf8..."
-
#
-
# Digest::SHA2.new(384).hexdigest 'abc' # => "cb00753f4..."
-
# Digest::SHA384.hexdigest 'abc' # => "cb00753f4..."
-
#
-
# Digest::SHA2.new(512).hexdigest 'abc' # => "ddaf35a19..."
-
# Digest::SHA512.hexdigest 'abc' # => "ddaf35a19..."
-
#
-
# # Compute digest by chunks
-
# sha2 = Digest::SHA2.new # =>#<Digest::SHA2:256>
-
# sha2.update "ab"
-
# sha2 << "c" # alias for #update
-
# sha2.hexdigest # => "ba7816bf8..."
-
#
-
# # Use the same object to compute another digest
-
# sha2.reset
-
# sha2 << "message"
-
# sha2.hexdigest # => "ab530a13e..."
-
#
-
1
class SHA2 < Digest::Class
-
# call-seq:
-
# Digest::SHA2.new(bitlen = 256) -> digest_obj
-
#
-
# Create a new SHA2 hash object with a given bit length.
-
#
-
# Valid bit lengths are 256, 384 and 512.
-
1
def initialize(bitlen = 256)
-
case bitlen
-
when 256
-
@sha2 = Digest::SHA256.new
-
when 384
-
@sha2 = Digest::SHA384.new
-
when 512
-
@sha2 = Digest::SHA512.new
-
else
-
raise ArgumentError, "unsupported bit length: %s" % bitlen.inspect
-
end
-
@bitlen = bitlen
-
end
-
-
# call-seq:
-
# digest_obj.reset -> digest_obj
-
#
-
# Reset the digest to the initial state and return self.
-
1
def reset
-
@sha2.reset
-
self
-
end
-
-
# call-seq:
-
# digest_obj.update(string) -> digest_obj
-
# digest_obj << string -> digest_obj
-
#
-
# Update the digest using a given _string_ and return self.
-
1
def update(str)
-
@sha2.update(str)
-
self
-
end
-
1
alias << update
-
-
1
def finish # :nodoc:
-
@sha2.digest!
-
end
-
1
private :finish
-
-
-
# call-seq:
-
# digest_obj.block_length -> Integer
-
#
-
# Return the block length of the digest in bytes.
-
#
-
# Digest::SHA256.new.block_length * 8
-
# # => 512
-
# Digest::SHA384.new.block_length * 8
-
# # => 1024
-
# Digest::SHA512.new.block_length * 8
-
# # => 1024
-
1
def block_length
-
@sha2.block_length
-
end
-
-
# call-seq:
-
# digest_obj.digest_length -> Integer
-
#
-
# Return the length of the hash value (the digest) in bytes.
-
#
-
# Digest::SHA256.new.digest_length * 8
-
# # => 256
-
# Digest::SHA384.new.digest_length * 8
-
# # => 384
-
# Digest::SHA512.new.digest_length * 8
-
# # => 512
-
#
-
# For example, digests produced by Digest::SHA256 will always be 32 bytes
-
# (256 bits) in size.
-
1
def digest_length
-
@sha2.digest_length
-
end
-
-
1
def initialize_copy(other) # :nodoc:
-
@sha2 = other.instance_eval { @sha2.clone }
-
end
-
-
1
def inspect # :nodoc:
-
"#<%s:%d %s>" % [self.class.name, @bitlen, hexdigest]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
#
-
# ipaddr.rb - A class to manipulate an IP address
-
#
-
# Copyright (c) 2002 Hajimu UMEMOTO <ume@mahoroba.org>.
-
# Copyright (c) 2007, 2009, 2012 Akinori MUSHA <knu@iDaemons.org>.
-
# All rights reserved.
-
#
-
# You can redistribute and/or modify it under the same terms as Ruby.
-
#
-
# $Id$
-
#
-
# Contact:
-
# - Akinori MUSHA <knu@iDaemons.org> (current maintainer)
-
#
-
# TODO:
-
# - scope_id support
-
#
-
1
require 'socket'
-
-
# IPAddr provides a set of methods to manipulate an IP address. Both IPv4 and
-
# IPv6 are supported.
-
#
-
# == Example
-
#
-
# require 'ipaddr'
-
#
-
# ipaddr1 = IPAddr.new "3ffe:505:2::1"
-
#
-
# p ipaddr1 #=> #<IPAddr: IPv6:3ffe:0505:0002:0000:0000:0000:0000:0001/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff>
-
#
-
# p ipaddr1.to_s #=> "3ffe:505:2::1"
-
#
-
# ipaddr2 = ipaddr1.mask(48) #=> #<IPAddr: IPv6:3ffe:0505:0002:0000:0000:0000:0000:0000/ffff:ffff:ffff:0000:0000:0000:0000:0000>
-
#
-
# p ipaddr2.to_s #=> "3ffe:505:2::"
-
#
-
# ipaddr3 = IPAddr.new "192.168.2.0/24"
-
#
-
# p ipaddr3 #=> #<IPAddr: IPv4:192.168.2.0/255.255.255.0>
-
-
1
class IPAddr
-
-
# 32 bit mask for IPv4
-
1
IN4MASK = 0xffffffff
-
# 128 bit mask for IPv6
-
1
IN6MASK = 0xffffffffffffffffffffffffffffffff
-
# Format string for IPv6
-
1
IN6FORMAT = (["%.4x"] * 8).join(':')
-
-
# Regexp _internally_ used for parsing IPv4 address.
-
1
RE_IPV4ADDRLIKE = %r{
-
\A
-
(\d+) \. (\d+) \. (\d+) \. (\d+)
-
\z
-
}x
-
-
# Regexp _internally_ used for parsing IPv6 address.
-
1
RE_IPV6ADDRLIKE_FULL = %r{
-
\A
-
(?:
-
(?: [\da-f]{1,4} : ){7} [\da-f]{1,4}
-
|
-
( (?: [\da-f]{1,4} : ){6} )
-
(\d+) \. (\d+) \. (\d+) \. (\d+)
-
)
-
\z
-
}xi
-
-
# Regexp _internally_ used for parsing IPv6 address.
-
1
RE_IPV6ADDRLIKE_COMPRESSED = %r{
-
\A
-
( (?: (?: [\da-f]{1,4} : )* [\da-f]{1,4} )? )
-
::
-
( (?:
-
( (?: [\da-f]{1,4} : )* )
-
(?:
-
[\da-f]{1,4}
-
|
-
(\d+) \. (\d+) \. (\d+) \. (\d+)
-
)
-
)? )
-
\z
-
}xi
-
-
# Generic IPAddr related error. Exceptions raised in this class should
-
# inherit from Error.
-
1
class Error < ArgumentError; end
-
-
# Raised when the provided IP address is an invalid address.
-
1
class InvalidAddressError < Error; end
-
-
# Raised when the address family is invalid such as an address with an
-
# unsupported family, an address with an inconsistent family, or an address
-
# who's family cannot be determined.
-
1
class AddressFamilyError < Error; end
-
-
# Raised when the address is an invalid length.
-
1
class InvalidPrefixError < InvalidAddressError; end
-
-
# Returns the address family of this IP address.
-
1
attr_reader :family
-
-
# Creates a new ipaddr containing the given network byte ordered
-
# string form of an IP address.
-
1
def self.new_ntoh(addr)
-
return new(ntop(addr))
-
end
-
-
# Convert a network byte ordered string form of an IP address into
-
# human readable form.
-
1
def self.ntop(addr)
-
case addr.size
-
when 4
-
s = addr.unpack('C4').join('.')
-
when 16
-
s = IN6FORMAT % addr.unpack('n8')
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
return s
-
end
-
-
# Returns a new ipaddr built by bitwise AND.
-
1
def &(other)
-
return self.clone.set(@addr & coerce_other(other).to_i)
-
end
-
-
# Returns a new ipaddr built by bitwise OR.
-
1
def |(other)
-
return self.clone.set(@addr | coerce_other(other).to_i)
-
end
-
-
# Returns a new ipaddr built by bitwise right-shift.
-
1
def >>(num)
-
return self.clone.set(@addr >> num)
-
end
-
-
# Returns a new ipaddr built by bitwise left shift.
-
1
def <<(num)
-
return self.clone.set(addr_mask(@addr << num))
-
end
-
-
# Returns a new ipaddr built by bitwise negation.
-
1
def ~
-
return self.clone.set(addr_mask(~@addr))
-
end
-
-
# Returns true if two ipaddrs are equal.
-
1
def ==(other)
-
other = coerce_other(other)
-
rescue
-
false
-
else
-
@family == other.family && @addr == other.to_i
-
end
-
-
# Returns a new ipaddr built by masking IP address with the given
-
# prefixlen/netmask. (e.g. 8, 64, "255.255.255.0", etc.)
-
1
def mask(prefixlen)
-
return self.clone.mask!(prefixlen)
-
end
-
-
# Returns true if the given ipaddr is in the range.
-
#
-
# e.g.:
-
# require 'ipaddr'
-
# net1 = IPAddr.new("192.168.2.0/24")
-
# net2 = IPAddr.new("192.168.2.100")
-
# net3 = IPAddr.new("192.168.3.0")
-
# p net1.include?(net2) #=> true
-
# p net1.include?(net3) #=> false
-
1
def include?(other)
-
other = coerce_other(other)
-
if ipv4_mapped?
-
if (@mask_addr >> 32) != 0xffffffffffffffffffffffff
-
return false
-
end
-
mask_addr = (@mask_addr & IN4MASK)
-
addr = (@addr & IN4MASK)
-
family = Socket::AF_INET
-
else
-
mask_addr = @mask_addr
-
addr = @addr
-
family = @family
-
end
-
if other.ipv4_mapped?
-
other_addr = (other.to_i & IN4MASK)
-
other_family = Socket::AF_INET
-
else
-
other_addr = other.to_i
-
other_family = other.family
-
end
-
-
if family != other_family
-
return false
-
end
-
return ((addr & mask_addr) == (other_addr & mask_addr))
-
end
-
1
alias === include?
-
-
# Returns the integer representation of the ipaddr.
-
1
def to_i
-
return @addr
-
end
-
-
# Returns a string containing the IP address representation.
-
1
def to_s
-
str = to_string
-
return str if ipv4?
-
-
str.gsub!(/\b0{1,3}([\da-f]+)\b/i, '\1')
-
loop do
-
break if str.sub!(/\A0:0:0:0:0:0:0:0\z/, '::')
-
break if str.sub!(/\b0:0:0:0:0:0:0\b/, ':')
-
break if str.sub!(/\b0:0:0:0:0:0\b/, ':')
-
break if str.sub!(/\b0:0:0:0:0\b/, ':')
-
break if str.sub!(/\b0:0:0:0\b/, ':')
-
break if str.sub!(/\b0:0:0\b/, ':')
-
break if str.sub!(/\b0:0\b/, ':')
-
break
-
end
-
str.sub!(/:{3,}/, '::')
-
-
if /\A::(ffff:)?([\da-f]{1,4}):([\da-f]{1,4})\z/i =~ str
-
str = sprintf('::%s%d.%d.%d.%d', $1, $2.hex / 256, $2.hex % 256, $3.hex / 256, $3.hex % 256)
-
end
-
-
str
-
end
-
-
# Returns a string containing the IP address representation in
-
# canonical form.
-
1
def to_string
-
return _to_string(@addr)
-
end
-
-
# Returns a network byte ordered string form of the IP address.
-
1
def hton
-
case @family
-
when Socket::AF_INET
-
return [@addr].pack('N')
-
when Socket::AF_INET6
-
return (0..7).map { |i|
-
(@addr >> (112 - 16 * i)) & 0xffff
-
}.pack('n8')
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
# Returns true if the ipaddr is an IPv4 address.
-
1
def ipv4?
-
return @family == Socket::AF_INET
-
end
-
-
# Returns true if the ipaddr is an IPv6 address.
-
1
def ipv6?
-
return @family == Socket::AF_INET6
-
end
-
-
# Returns true if the ipaddr is a loopback address.
-
1
def loopback?
-
case @family
-
when Socket::AF_INET
-
@addr & 0xff000000 == 0x7f000000
-
when Socket::AF_INET6
-
@addr == 1
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
# Returns true if the ipaddr is a private address. IPv4 addresses
-
# in 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16 as defined in RFC
-
# 1918 and IPv6 Unique Local Addresses in fc00::/7 as defined in RFC
-
# 4193 are considered private.
-
1
def private?
-
case @family
-
when Socket::AF_INET
-
@addr & 0xff000000 == 0x0a000000 || # 10.0.0.0/8
-
@addr & 0xfff00000 == 0xac100000 || # 172.16.0.0/12
-
@addr & 0xffff0000 == 0xc0a80000 # 192.168.0.0/16
-
when Socket::AF_INET6
-
@addr & 0xfe00_0000_0000_0000_0000_0000_0000_0000 == 0xfc00_0000_0000_0000_0000_0000_0000_0000
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
# Returns true if the ipaddr is a link-local address. IPv4
-
# addresses in 169.254.0.0/16 reserved by RFC 3927 and Link-Local
-
# IPv6 Unicast Addresses in fe80::/10 reserved by RFC 4291 are
-
# considered link-local.
-
1
def link_local?
-
case @family
-
when Socket::AF_INET
-
@addr & 0xffff0000 == 0xa9fe0000 # 169.254.0.0/16
-
when Socket::AF_INET6
-
@addr & 0xffc0_0000_0000_0000_0000_0000_0000_0000 == 0xfe80_0000_0000_0000_0000_0000_0000_0000
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
# Returns true if the ipaddr is an IPv4-mapped IPv6 address.
-
1
def ipv4_mapped?
-
return ipv6? && (@addr >> 32) == 0xffff
-
end
-
-
# Returns true if the ipaddr is an IPv4-compatible IPv6 address.
-
1
def ipv4_compat?
-
warn "IPAddr\##{__callee__} is obsolete", uplevel: 1 if $VERBOSE
-
_ipv4_compat?
-
end
-
-
1
def _ipv4_compat?
-
if !ipv6? || (@addr >> 32) != 0
-
return false
-
end
-
a = (@addr & IN4MASK)
-
return a != 0 && a != 1
-
end
-
-
1
private :_ipv4_compat?
-
-
# Returns a new ipaddr built by converting the native IPv4 address
-
# into an IPv4-mapped IPv6 address.
-
1
def ipv4_mapped
-
if !ipv4?
-
raise InvalidAddressError, "not an IPv4 address"
-
end
-
return self.clone.set(@addr | 0xffff00000000, Socket::AF_INET6)
-
end
-
-
# Returns a new ipaddr built by converting the native IPv4 address
-
# into an IPv4-compatible IPv6 address.
-
1
def ipv4_compat
-
warn "IPAddr\##{__callee__} is obsolete", uplevel: 1 if $VERBOSE
-
if !ipv4?
-
raise InvalidAddressError, "not an IPv4 address"
-
end
-
return self.clone.set(@addr, Socket::AF_INET6)
-
end
-
-
# Returns a new ipaddr built by converting the IPv6 address into a
-
# native IPv4 address. If the IP address is not an IPv4-mapped or
-
# IPv4-compatible IPv6 address, returns self.
-
1
def native
-
if !ipv4_mapped? && !_ipv4_compat?
-
return self
-
end
-
return self.clone.set(@addr & IN4MASK, Socket::AF_INET)
-
end
-
-
# Returns a string for DNS reverse lookup. It returns a string in
-
# RFC3172 form for an IPv6 address.
-
1
def reverse
-
case @family
-
when Socket::AF_INET
-
return _reverse + ".in-addr.arpa"
-
when Socket::AF_INET6
-
return ip6_arpa
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
# Returns a string for DNS reverse lookup compatible with RFC3172.
-
1
def ip6_arpa
-
if !ipv6?
-
raise InvalidAddressError, "not an IPv6 address"
-
end
-
return _reverse + ".ip6.arpa"
-
end
-
-
# Returns a string for DNS reverse lookup compatible with RFC1886.
-
1
def ip6_int
-
if !ipv6?
-
raise InvalidAddressError, "not an IPv6 address"
-
end
-
return _reverse + ".ip6.int"
-
end
-
-
# Returns the successor to the ipaddr.
-
1
def succ
-
return self.clone.set(@addr + 1, @family)
-
end
-
-
# Compares the ipaddr with another.
-
1
def <=>(other)
-
other = coerce_other(other)
-
rescue
-
nil
-
else
-
@addr <=> other.to_i if other.family == @family
-
end
-
1
include Comparable
-
-
# Checks equality used by Hash.
-
1
def eql?(other)
-
return self.class == other.class && self.hash == other.hash && self == other
-
end
-
-
# Returns a hash value used by Hash, Set, and Array classes
-
1
def hash
-
return ([@addr, @mask_addr].hash << 1) | (ipv4? ? 0 : 1)
-
end
-
-
# Creates a Range object for the network address.
-
1
def to_range
-
begin_addr = (@addr & @mask_addr)
-
-
case @family
-
when Socket::AF_INET
-
end_addr = (@addr | (IN4MASK ^ @mask_addr))
-
when Socket::AF_INET6
-
end_addr = (@addr | (IN6MASK ^ @mask_addr))
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
-
return clone.set(begin_addr, @family)..clone.set(end_addr, @family)
-
end
-
-
# Returns the prefix length in bits for the ipaddr.
-
1
def prefix
-
case @family
-
when Socket::AF_INET
-
n = IN4MASK ^ @mask_addr
-
i = 32
-
when Socket::AF_INET6
-
n = IN6MASK ^ @mask_addr
-
i = 128
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
while n.positive?
-
n >>= 1
-
i -= 1
-
end
-
i
-
end
-
-
# Sets the prefix length in bits
-
1
def prefix=(prefix)
-
case prefix
-
when Integer
-
mask!(prefix)
-
else
-
raise InvalidPrefixError, "prefix must be an integer"
-
end
-
end
-
-
# Returns a string containing a human-readable representation of the
-
# ipaddr. ("#<IPAddr: family:address/mask>")
-
1
def inspect
-
case @family
-
when Socket::AF_INET
-
af = "IPv4"
-
when Socket::AF_INET6
-
af = "IPv6"
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
return sprintf("#<%s: %s:%s/%s>", self.class.name,
-
af, _to_string(@addr), _to_string(@mask_addr))
-
end
-
-
1
protected
-
-
# Set +@addr+, the internal stored ip address, to given +addr+. The
-
# parameter +addr+ is validated using the first +family+ member,
-
# which is +Socket::AF_INET+ or +Socket::AF_INET6+.
-
1
def set(addr, *family)
-
case family[0] ? family[0] : @family
-
when Socket::AF_INET
-
if addr < 0 || addr > IN4MASK
-
raise InvalidAddressError, "invalid address"
-
end
-
when Socket::AF_INET6
-
if addr < 0 || addr > IN6MASK
-
raise InvalidAddressError, "invalid address"
-
end
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
@addr = addr
-
if family[0]
-
@family = family[0]
-
end
-
return self
-
end
-
-
# Set current netmask to given mask.
-
1
def mask!(mask)
-
case mask
-
when String
-
if mask =~ /\A\d+\z/
-
prefixlen = mask.to_i
-
else
-
m = IPAddr.new(mask)
-
if m.family != @family
-
raise InvalidPrefixError, "address family is not same"
-
end
-
@mask_addr = m.to_i
-
n = @mask_addr ^ m.instance_variable_get(:@mask_addr)
-
unless ((n + 1) & n).zero?
-
raise InvalidPrefixError, "invalid mask #{mask}"
-
end
-
@addr &= @mask_addr
-
return self
-
end
-
else
-
prefixlen = mask
-
end
-
case @family
-
when Socket::AF_INET
-
if prefixlen < 0 || prefixlen > 32
-
raise InvalidPrefixError, "invalid length"
-
end
-
masklen = 32 - prefixlen
-
@mask_addr = ((IN4MASK >> masklen) << masklen)
-
when Socket::AF_INET6
-
if prefixlen < 0 || prefixlen > 128
-
raise InvalidPrefixError, "invalid length"
-
end
-
masklen = 128 - prefixlen
-
@mask_addr = ((IN6MASK >> masklen) << masklen)
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
@addr = ((@addr >> masklen) << masklen)
-
return self
-
end
-
-
1
private
-
-
# Creates a new ipaddr object either from a human readable IP
-
# address representation in string, or from a packed in_addr value
-
# followed by an address family.
-
#
-
# In the former case, the following are the valid formats that will
-
# be recognized: "address", "address/prefixlen" and "address/mask",
-
# where IPv6 address may be enclosed in square brackets (`[' and
-
# `]'). If a prefixlen or a mask is specified, it returns a masked
-
# IP address. Although the address family is determined
-
# automatically from a specified string, you can specify one
-
# explicitly by the optional second argument.
-
#
-
# Otherwise an IP address is generated from a packed in_addr value
-
# and an address family.
-
#
-
# The IPAddr class defines many methods and operators, and some of
-
# those, such as &, |, include? and ==, accept a string, or a packed
-
# in_addr value instead of an IPAddr object.
-
1
def initialize(addr = '::', family = Socket::AF_UNSPEC)
-
if !addr.kind_of?(String)
-
case family
-
when Socket::AF_INET, Socket::AF_INET6
-
set(addr.to_i, family)
-
@mask_addr = (family == Socket::AF_INET) ? IN4MASK : IN6MASK
-
return
-
when Socket::AF_UNSPEC
-
raise AddressFamilyError, "address family must be specified"
-
else
-
raise AddressFamilyError, "unsupported address family: #{family}"
-
end
-
end
-
prefix, prefixlen = addr.split('/')
-
if prefix =~ /\A\[(.*)\]\z/i
-
prefix = $1
-
family = Socket::AF_INET6
-
end
-
# It seems AI_NUMERICHOST doesn't do the job.
-
#Socket.getaddrinfo(left, nil, Socket::AF_INET6, Socket::SOCK_STREAM, nil,
-
# Socket::AI_NUMERICHOST)
-
@addr = @family = nil
-
if family == Socket::AF_UNSPEC || family == Socket::AF_INET
-
@addr = in_addr(prefix)
-
if @addr
-
@family = Socket::AF_INET
-
end
-
end
-
if !@addr && (family == Socket::AF_UNSPEC || family == Socket::AF_INET6)
-
@addr = in6_addr(prefix)
-
@family = Socket::AF_INET6
-
end
-
if family != Socket::AF_UNSPEC && @family != family
-
raise AddressFamilyError, "address family mismatch"
-
end
-
if prefixlen
-
mask!(prefixlen)
-
else
-
@mask_addr = (@family == Socket::AF_INET) ? IN4MASK : IN6MASK
-
end
-
rescue InvalidAddressError => e
-
raise e.class, "#{e.message}: #{addr}"
-
end
-
-
1
def coerce_other(other)
-
case other
-
when IPAddr
-
other
-
when String
-
self.class.new(other)
-
else
-
self.class.new(other, @family)
-
end
-
end
-
-
1
def in_addr(addr)
-
case addr
-
when Array
-
octets = addr
-
else
-
m = RE_IPV4ADDRLIKE.match(addr) or return nil
-
octets = m.captures
-
end
-
octets.inject(0) { |i, s|
-
(n = s.to_i) < 256 or raise InvalidAddressError, "invalid address"
-
s.match(/\A0./) and raise InvalidAddressError, "zero-filled number in IPv4 address is ambiguous"
-
i << 8 | n
-
}
-
end
-
-
1
def in6_addr(left)
-
case left
-
when RE_IPV6ADDRLIKE_FULL
-
if $2
-
addr = in_addr($~[2,4])
-
left = $1 + ':'
-
else
-
addr = 0
-
end
-
right = ''
-
when RE_IPV6ADDRLIKE_COMPRESSED
-
if $4
-
left.count(':') <= 6 or raise InvalidAddressError, "invalid address"
-
addr = in_addr($~[4,4])
-
left = $1
-
right = $3 + '0:0'
-
else
-
left.count(':') <= ($1.empty? || $2.empty? ? 8 : 7) or
-
raise InvalidAddressError, "invalid address"
-
left = $1
-
right = $2
-
addr = 0
-
end
-
else
-
raise InvalidAddressError, "invalid address"
-
end
-
l = left.split(':')
-
r = right.split(':')
-
rest = 8 - l.size - r.size
-
if rest < 0
-
return nil
-
end
-
(l + Array.new(rest, '0') + r).inject(0) { |i, s|
-
i << 16 | s.hex
-
} | addr
-
end
-
-
1
def addr_mask(addr)
-
case @family
-
when Socket::AF_INET
-
return addr & IN4MASK
-
when Socket::AF_INET6
-
return addr & IN6MASK
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
1
def _reverse
-
case @family
-
when Socket::AF_INET
-
return (0..3).map { |i|
-
(@addr >> (8 * i)) & 0xff
-
}.join('.')
-
when Socket::AF_INET6
-
return ("%.32x" % @addr).reverse!.gsub!(/.(?!$)/, '\&.')
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
1
def _to_string(addr)
-
case @family
-
when Socket::AF_INET
-
return (0..3).map { |i|
-
(addr >> (24 - 8 * i)) & 0xff
-
}.join('.')
-
when Socket::AF_INET6
-
return (("%.32x" % addr).gsub!(/.{4}(?!$)/, '\&:'))
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
end
-
-
1
unless Socket.const_defined? :AF_INET6
-
class Socket < BasicSocket
-
# IPv6 protocol family
-
AF_INET6 = Object.new
-
end
-
-
class << IPSocket
-
private
-
-
def valid_v6?(addr)
-
case addr
-
when IPAddr::RE_IPV6ADDRLIKE_FULL
-
if $2
-
$~[2,4].all? {|i| i.to_i < 256 }
-
else
-
true
-
end
-
when IPAddr::RE_IPV6ADDRLIKE_COMPRESSED
-
if $4
-
addr.count(':') <= 6 && $~[4,4].all? {|i| i.to_i < 256}
-
else
-
addr.count(':') <= 7
-
end
-
else
-
false
-
end
-
end
-
-
alias getaddress_orig getaddress
-
-
public
-
-
# Returns a +String+ based representation of a valid DNS hostname,
-
# IPv4 or IPv6 address.
-
#
-
# IPSocket.getaddress 'localhost' #=> "::1"
-
# IPSocket.getaddress 'broadcasthost' #=> "255.255.255.255"
-
# IPSocket.getaddress 'www.ruby-lang.org' #=> "221.186.184.68"
-
# IPSocket.getaddress 'www.ccc.de' #=> "2a00:1328:e102:ccc0::122"
-
def getaddress(s)
-
if valid_v6?(s)
-
s
-
else
-
getaddress_orig(s)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
# logger.rb - simple logging utility
-
# Copyright (C) 2000-2003, 2005, 2008, 2011 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
-
#
-
# Documentation:: NAKAMURA, Hiroshi and Gavin Sinclair
-
# License::
-
# You can redistribute it and/or modify it under the same terms of Ruby's
-
# license; either the dual license version in 2003, or any later version.
-
# Revision:: $Id$
-
#
-
# A simple system for logging messages. See Logger for more documentation.
-
-
1
require 'monitor'
-
-
1
require_relative 'logger/version'
-
1
require_relative 'logger/formatter'
-
1
require_relative 'logger/log_device'
-
1
require_relative 'logger/severity'
-
1
require_relative 'logger/errors'
-
-
# == Description
-
#
-
# The Logger class provides a simple but sophisticated logging utility that
-
# you can use to output messages.
-
#
-
# The messages have associated levels, such as +INFO+ or +ERROR+ that indicate
-
# their importance. You can then give the Logger a level, and only messages
-
# at that level or higher will be printed.
-
#
-
# The levels are:
-
#
-
# +UNKNOWN+:: An unknown message that should always be logged.
-
# +FATAL+:: An unhandleable error that results in a program crash.
-
# +ERROR+:: A handleable error condition.
-
# +WARN+:: A warning.
-
# +INFO+:: Generic (useful) information about system operation.
-
# +DEBUG+:: Low-level information for developers.
-
#
-
# For instance, in a production system, you may have your Logger set to
-
# +INFO+ or even +WARN+.
-
# When you are developing the system, however, you probably
-
# want to know about the program's internal state, and would set the Logger to
-
# +DEBUG+.
-
#
-
# *Note*: Logger does not escape or sanitize any messages passed to it.
-
# Developers should be aware of when potentially malicious data (user-input)
-
# is passed to Logger, and manually escape the untrusted data:
-
#
-
# logger.info("User-input: #{input.dump}")
-
# logger.info("User-input: %p" % input)
-
#
-
# You can use #formatter= for escaping all data.
-
#
-
# original_formatter = Logger::Formatter.new
-
# logger.formatter = proc { |severity, datetime, progname, msg|
-
# original_formatter.call(severity, datetime, progname, msg.dump)
-
# }
-
# logger.info(input)
-
#
-
# === Example
-
#
-
# This creates a Logger that outputs to the standard output stream, with a
-
# level of +WARN+:
-
#
-
# require 'logger'
-
#
-
# logger = Logger.new(STDOUT)
-
# logger.level = Logger::WARN
-
#
-
# logger.debug("Created logger")
-
# logger.info("Program started")
-
# logger.warn("Nothing to do!")
-
#
-
# path = "a_non_existent_file"
-
#
-
# begin
-
# File.foreach(path) do |line|
-
# unless line =~ /^(\w+) = (.*)$/
-
# logger.error("Line in wrong format: #{line.chomp}")
-
# end
-
# end
-
# rescue => err
-
# logger.fatal("Caught exception; exiting")
-
# logger.fatal(err)
-
# end
-
#
-
# Because the Logger's level is set to +WARN+, only the warning, error, and
-
# fatal messages are recorded. The debug and info messages are silently
-
# discarded.
-
#
-
# === Features
-
#
-
# There are several interesting features that Logger provides, like
-
# auto-rolling of log files, setting the format of log messages, and
-
# specifying a program name in conjunction with the message. The next section
-
# shows you how to achieve these things.
-
#
-
#
-
# == HOWTOs
-
#
-
# === How to create a logger
-
#
-
# The options below give you various choices, in more or less increasing
-
# complexity.
-
#
-
# 1. Create a logger which logs messages to STDERR/STDOUT.
-
#
-
# logger = Logger.new(STDERR)
-
# logger = Logger.new(STDOUT)
-
#
-
# 2. Create a logger for the file which has the specified name.
-
#
-
# logger = Logger.new('logfile.log')
-
#
-
# 3. Create a logger for the specified file.
-
#
-
# file = File.open('foo.log', File::WRONLY | File::APPEND)
-
# # To create new logfile, add File::CREAT like:
-
# # file = File.open('foo.log', File::WRONLY | File::APPEND | File::CREAT)
-
# logger = Logger.new(file)
-
#
-
# 4. Create a logger which ages the logfile once it reaches a certain size.
-
# Leave 10 "old" log files where each file is about 1,024,000 bytes.
-
#
-
# logger = Logger.new('foo.log', 10, 1024000)
-
#
-
# 5. Create a logger which ages the logfile daily/weekly/monthly.
-
#
-
# logger = Logger.new('foo.log', 'daily')
-
# logger = Logger.new('foo.log', 'weekly')
-
# logger = Logger.new('foo.log', 'monthly')
-
#
-
# === How to log a message
-
#
-
# Notice the different methods (+fatal+, +error+, +info+) being used to log
-
# messages of various levels? Other methods in this family are +warn+ and
-
# +debug+. +add+ is used below to log a message of an arbitrary (perhaps
-
# dynamic) level.
-
#
-
# 1. Message in a block.
-
#
-
# logger.fatal { "Argument 'foo' not given." }
-
#
-
# 2. Message as a string.
-
#
-
# logger.error "Argument #{@foo} mismatch."
-
#
-
# 3. With progname.
-
#
-
# logger.info('initialize') { "Initializing..." }
-
#
-
# 4. With severity.
-
#
-
# logger.add(Logger::FATAL) { 'Fatal error!' }
-
#
-
# The block form allows you to create potentially complex log messages,
-
# but to delay their evaluation until and unless the message is
-
# logged. For example, if we have the following:
-
#
-
# logger.debug { "This is a " + potentially + " expensive operation" }
-
#
-
# If the logger's level is +INFO+ or higher, no debug messages will be logged,
-
# and the entire block will not even be evaluated. Compare to this:
-
#
-
# logger.debug("This is a " + potentially + " expensive operation")
-
#
-
# Here, the string concatenation is done every time, even if the log
-
# level is not set to show the debug message.
-
#
-
# === How to close a logger
-
#
-
# logger.close
-
#
-
# === Setting severity threshold
-
#
-
# 1. Original interface.
-
#
-
# logger.sev_threshold = Logger::WARN
-
#
-
# 2. Log4r (somewhat) compatible interface.
-
#
-
# logger.level = Logger::INFO
-
#
-
# # DEBUG < INFO < WARN < ERROR < FATAL < UNKNOWN
-
#
-
# 3. Symbol or String (case insensitive)
-
#
-
# logger.level = :info
-
# logger.level = 'INFO'
-
#
-
# # :debug < :info < :warn < :error < :fatal < :unknown
-
#
-
# 4. Constructor
-
#
-
# Logger.new(logdev, level: Logger::INFO)
-
# Logger.new(logdev, level: :info)
-
# Logger.new(logdev, level: 'INFO')
-
#
-
# == Format
-
#
-
# Log messages are rendered in the output stream in a certain format by
-
# default. The default format and a sample are shown below:
-
#
-
# Log format:
-
# SeverityID, [DateTime #pid] SeverityLabel -- ProgName: message
-
#
-
# Log sample:
-
# I, [1999-03-03T02:34:24.895701 #19074] INFO -- Main: info.
-
#
-
# You may change the date and time format via #datetime_format=.
-
#
-
# logger.datetime_format = '%Y-%m-%d %H:%M:%S'
-
# # e.g. "2004-01-03 00:54:26"
-
#
-
# or via the constructor.
-
#
-
# Logger.new(logdev, datetime_format: '%Y-%m-%d %H:%M:%S')
-
#
-
# Or, you may change the overall format via the #formatter= method.
-
#
-
# logger.formatter = proc do |severity, datetime, progname, msg|
-
# "#{datetime}: #{msg}\n"
-
# end
-
# # e.g. "2005-09-22 08:51:08 +0900: hello world"
-
#
-
# or via the constructor.
-
#
-
# Logger.new(logdev, formatter: proc {|severity, datetime, progname, msg|
-
# "#{datetime}: #{msg}\n"
-
# })
-
#
-
1
class Logger
-
1
_, name, rev = %w$Id$
-
1
if name
-
name = name.chomp(",v")
-
else
-
1
name = File.basename(__FILE__)
-
end
-
1
rev ||= "v#{VERSION}"
-
1
ProgName = "#{name}/#{rev}"
-
-
1
include Severity
-
-
# Logging severity threshold (e.g. <tt>Logger::INFO</tt>).
-
1
attr_reader :level
-
-
# Set logging severity threshold.
-
#
-
# +severity+:: The Severity of the log message.
-
1
def level=(severity)
-
2
if severity.is_a?(Integer)
-
2
@level = severity
-
else
-
case severity.to_s.downcase
-
when 'debug'
-
@level = DEBUG
-
when 'info'
-
@level = INFO
-
when 'warn'
-
@level = WARN
-
when 'error'
-
@level = ERROR
-
when 'fatal'
-
@level = FATAL
-
when 'unknown'
-
@level = UNKNOWN
-
else
-
raise ArgumentError, "invalid log level: #{severity}"
-
end
-
end
-
end
-
-
# Program name to include in log messages.
-
1
attr_accessor :progname
-
-
# Set date-time format.
-
#
-
# +datetime_format+:: A string suitable for passing to +strftime+.
-
1
def datetime_format=(datetime_format)
-
1
@default_formatter.datetime_format = datetime_format
-
end
-
-
# Returns the date format being used. See #datetime_format=
-
1
def datetime_format
-
@default_formatter.datetime_format
-
end
-
-
# Logging formatter, as a +Proc+ that will take four arguments and
-
# return the formatted message. The arguments are:
-
#
-
# +severity+:: The Severity of the log message.
-
# +time+:: A Time instance representing when the message was logged.
-
# +progname+:: The #progname configured, or passed to the logger method.
-
# +msg+:: The _Object_ the user passed to the log message; not necessarily a
-
# String.
-
#
-
# The block should return an Object that can be written to the logging
-
# device via +write+. The default formatter is used when no formatter is
-
# set.
-
1
attr_accessor :formatter
-
-
1
alias sev_threshold level
-
1
alias sev_threshold= level=
-
-
# Returns +true+ iff the current severity level allows for the printing of
-
# +DEBUG+ messages.
-
1
def debug?; level <= DEBUG; end
-
-
# Sets the severity to DEBUG.
-
1
def debug!; self.level = DEBUG; end
-
-
# Returns +true+ iff the current severity level allows for the printing of
-
# +INFO+ messages.
-
1
def info?; level <= INFO; end
-
-
# Sets the severity to INFO.
-
1
def info!; self.level = INFO; end
-
-
# Returns +true+ iff the current severity level allows for the printing of
-
# +WARN+ messages.
-
1
def warn?; level <= WARN; end
-
-
# Sets the severity to WARN.
-
1
def warn!; self.level = WARN; end
-
-
# Returns +true+ iff the current severity level allows for the printing of
-
# +ERROR+ messages.
-
1
def error?; level <= ERROR; end
-
-
# Sets the severity to ERROR.
-
1
def error!; self.level = ERROR; end
-
-
# Returns +true+ iff the current severity level allows for the printing of
-
# +FATAL+ messages.
-
1
def fatal?; level <= FATAL; end
-
-
# Sets the severity to FATAL.
-
1
def fatal!; self.level = FATAL; end
-
-
#
-
# :call-seq:
-
# Logger.new(logdev, shift_age = 0, shift_size = 1048576)
-
# Logger.new(logdev, shift_age = 'weekly')
-
# Logger.new(logdev, level: :info)
-
# Logger.new(logdev, progname: 'progname')
-
# Logger.new(logdev, formatter: formatter)
-
# Logger.new(logdev, datetime_format: '%Y-%m-%d %H:%M:%S')
-
#
-
# === Args
-
#
-
# +logdev+::
-
# The log device. This is a filename (String) or IO object (typically
-
# +STDOUT+, +STDERR+, or an open file).
-
# +shift_age+::
-
# Number of old log files to keep, *or* frequency of rotation (+daily+,
-
# +weekly+ or +monthly+). Default value is 0, which disables log file
-
# rotation.
-
# +shift_size+::
-
# Maximum logfile size in bytes (only applies when +shift_age+ is a positive
-
# Integer). Defaults to +1048576+ (1MB).
-
# +level+::
-
# Logging severity threshold. Default values is Logger::DEBUG.
-
# +progname+::
-
# Program name to include in log messages. Default value is nil.
-
# +formatter+::
-
# Logging formatter. Default values is an instance of Logger::Formatter.
-
# +datetime_format+::
-
# Date and time format. Default value is '%Y-%m-%d %H:%M:%S'.
-
# +binmode+::
-
# Use binary mode on the log device. Default value is false.
-
# +shift_period_suffix+::
-
# The log file suffix format for +daily+, +weekly+ or +monthly+ rotation.
-
# Default is '%Y%m%d'.
-
#
-
# === Description
-
#
-
# Create an instance.
-
#
-
1
def initialize(logdev, shift_age = 0, shift_size = 1048576, level: DEBUG,
-
progname: nil, formatter: nil, datetime_format: nil,
-
binmode: false, shift_period_suffix: '%Y%m%d')
-
1
self.level = level
-
1
self.progname = progname
-
1
@default_formatter = Formatter.new
-
1
self.datetime_format = datetime_format
-
1
self.formatter = formatter
-
1
@logdev = nil
-
1
if logdev
-
1
@logdev = LogDevice.new(logdev, shift_age: shift_age,
-
shift_size: shift_size,
-
shift_period_suffix: shift_period_suffix,
-
binmode: binmode)
-
end
-
end
-
-
#
-
# :call-seq:
-
# Logger#reopen
-
# Logger#reopen(logdev)
-
#
-
# === Args
-
#
-
# +logdev+::
-
# The log device. This is a filename (String) or IO object (typically
-
# +STDOUT+, +STDERR+, or an open file). reopen the same filename if
-
# it is +nil+, do nothing for IO. Default is +nil+.
-
#
-
# === Description
-
#
-
# Reopen a log device.
-
#
-
1
def reopen(logdev = nil)
-
@logdev.reopen(logdev)
-
self
-
end
-
-
#
-
# :call-seq:
-
# Logger#add(severity, message = nil, progname = nil) { ... }
-
#
-
# === Args
-
#
-
# +severity+::
-
# Severity. Constants are defined in Logger namespace: +DEBUG+, +INFO+,
-
# +WARN+, +ERROR+, +FATAL+, or +UNKNOWN+.
-
# +message+::
-
# The log message. A String or Exception.
-
# +progname+::
-
# Program name string. Can be omitted. Treated as a message if no
-
# +message+ and +block+ are given.
-
# +block+::
-
# Can be omitted. Called to get a message string if +message+ is nil.
-
#
-
# === Return
-
#
-
# When the given severity is not high enough (for this particular logger),
-
# log no message, and return +true+.
-
#
-
# === Description
-
#
-
# Log a message if the given severity is high enough. This is the generic
-
# logging method. Users will be more inclined to use #debug, #info, #warn,
-
# #error, and #fatal.
-
#
-
# <b>Message format</b>: +message+ can be any object, but it has to be
-
# converted to a String in order to log it. Generally, +inspect+ is used
-
# if the given object is not a String.
-
# A special case is an +Exception+ object, which will be printed in detail,
-
# including message, class, and backtrace. See #msg2str for the
-
# implementation if required.
-
#
-
# === Bugs
-
#
-
# * Logfile is not locked.
-
# * Append open does not need to lock file.
-
# * If the OS supports multi I/O, records possibly may be mixed.
-
#
-
1
def add(severity, message = nil, progname = nil)
-
16
severity ||= UNKNOWN
-
16
if @logdev.nil? or severity < level
-
16
return true
-
end
-
if progname.nil?
-
progname = @progname
-
end
-
if message.nil?
-
if block_given?
-
message = yield
-
else
-
message = progname
-
progname = @progname
-
end
-
end
-
@logdev.write(
-
format_message(format_severity(severity), Time.now, progname, message))
-
true
-
end
-
1
alias log add
-
-
#
-
# Dump given message to the log device without any formatting. If no log
-
# device exists, return +nil+.
-
#
-
1
def <<(msg)
-
@logdev&.write(msg)
-
end
-
-
#
-
# Log a +DEBUG+ message.
-
#
-
# See #info for more information.
-
#
-
1
def debug(progname = nil, &block)
-
14
add(DEBUG, nil, progname, &block)
-
end
-
-
#
-
# :call-seq:
-
# info(message)
-
# info(progname, &block)
-
#
-
# Log an +INFO+ message.
-
#
-
# +message+:: The message to log; does not need to be a String.
-
# +progname+:: In the block form, this is the #progname to use in the
-
# log message. The default can be set with #progname=.
-
# +block+:: Evaluates to the message to log. This is not evaluated unless
-
# the logger's level is sufficient to log the message. This
-
# allows you to create potentially expensive logging messages that
-
# are only called when the logger is configured to show them.
-
#
-
# === Examples
-
#
-
# logger.info("MainApp") { "Received connection from #{ip}" }
-
# # ...
-
# logger.info "Waiting for input from user"
-
# # ...
-
# logger.info { "User typed #{input}" }
-
#
-
# You'll probably stick to the second form above, unless you want to provide a
-
# program name (which you can do with #progname= as well).
-
#
-
# === Return
-
#
-
# See #add.
-
#
-
1
def info(progname = nil, &block)
-
2
add(INFO, nil, progname, &block)
-
end
-
-
#
-
# Log a +WARN+ message.
-
#
-
# See #info for more information.
-
#
-
1
def warn(progname = nil, &block)
-
add(WARN, nil, progname, &block)
-
end
-
-
#
-
# Log an +ERROR+ message.
-
#
-
# See #info for more information.
-
#
-
1
def error(progname = nil, &block)
-
add(ERROR, nil, progname, &block)
-
end
-
-
#
-
# Log a +FATAL+ message.
-
#
-
# See #info for more information.
-
#
-
1
def fatal(progname = nil, &block)
-
add(FATAL, nil, progname, &block)
-
end
-
-
#
-
# Log an +UNKNOWN+ message. This will be printed no matter what the logger's
-
# level is.
-
#
-
# See #info for more information.
-
#
-
1
def unknown(progname = nil, &block)
-
add(UNKNOWN, nil, progname, &block)
-
end
-
-
#
-
# Close the logging device.
-
#
-
1
def close
-
@logdev&.close
-
end
-
-
1
private
-
-
# Severity label for logging (max 5 chars).
-
1
SEV_LABEL = %w(DEBUG INFO WARN ERROR FATAL ANY).freeze
-
-
1
def format_severity(severity)
-
SEV_LABEL[severity] || 'ANY'
-
end
-
-
1
def format_message(severity, datetime, progname, msg)
-
(@formatter || @default_formatter).call(severity, datetime, progname, msg)
-
end
-
end
-
# frozen_string_literal: true
-
-
# not used after 1.2.7. just for compat.
-
1
class Logger
-
1
class Error < RuntimeError # :nodoc:
-
end
-
1
class ShiftingError < Error # :nodoc:
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Logger
-
# Default formatter for log messages.
-
1
class Formatter
-
1
Format = "%s, [%s#%d] %5s -- %s: %s\n"
-
-
1
attr_accessor :datetime_format
-
-
1
def initialize
-
1
@datetime_format = nil
-
end
-
-
1
def call(severity, time, progname, msg)
-
Format % [severity[0..0], format_datetime(time), $$, severity, progname,
-
msg2str(msg)]
-
end
-
-
1
private
-
-
1
def format_datetime(time)
-
time.strftime(@datetime_format || "%Y-%m-%dT%H:%M:%S.%6N ")
-
end
-
-
1
def msg2str(msg)
-
case msg
-
when ::String
-
msg
-
when ::Exception
-
"#{ msg.message } (#{ msg.class })\n#{ msg.backtrace.join("\n") if msg.backtrace }"
-
else
-
msg.inspect
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require_relative 'period'
-
-
1
class Logger
-
# Device used for logging messages.
-
1
class LogDevice
-
1
include Period
-
-
1
attr_reader :dev
-
1
attr_reader :filename
-
1
include MonitorMixin
-
-
1
def initialize(log = nil, shift_age: nil, shift_size: nil, shift_period_suffix: nil, binmode: false)
-
1
@dev = @filename = @shift_age = @shift_size = @shift_period_suffix = nil
-
1
@binmode = binmode
-
1
mon_initialize
-
1
set_dev(log)
-
1
if @filename
-
@shift_age = shift_age || 7
-
@shift_size = shift_size || 1048576
-
@shift_period_suffix = shift_period_suffix || '%Y%m%d'
-
-
unless @shift_age.is_a?(Integer)
-
base_time = @dev.respond_to?(:stat) ? @dev.stat.mtime : Time.now
-
@next_rotate_time = next_rotate_time(base_time, @shift_age)
-
end
-
end
-
end
-
-
1
def write(message)
-
begin
-
synchronize do
-
if @shift_age and @dev.respond_to?(:stat)
-
begin
-
check_shift_log
-
rescue
-
warn("log shifting failed. #{$!}")
-
end
-
end
-
begin
-
@dev.write(message)
-
rescue
-
warn("log writing failed. #{$!}")
-
end
-
end
-
rescue Exception => ignored
-
warn("log writing failed. #{ignored}")
-
end
-
end
-
-
1
def close
-
begin
-
synchronize do
-
@dev.close rescue nil
-
end
-
rescue Exception
-
@dev.close rescue nil
-
end
-
end
-
-
1
def reopen(log = nil)
-
# reopen the same filename if no argument, do nothing for IO
-
log ||= @filename if @filename
-
if log
-
synchronize do
-
if @filename and @dev
-
@dev.close rescue nil # close only file opened by Logger
-
@filename = nil
-
end
-
set_dev(log)
-
end
-
end
-
self
-
end
-
-
1
private
-
-
1
def set_dev(log)
-
1
if log.respond_to?(:write) and log.respond_to?(:close)
-
1
@dev = log
-
1
if log.respond_to?(:path)
-
@filename = log.path
-
end
-
else
-
@dev = open_logfile(log)
-
@dev.sync = true
-
@dev.binmode if @binmode
-
@filename = log
-
end
-
end
-
-
1
def open_logfile(filename)
-
begin
-
File.open(filename, (File::WRONLY | File::APPEND))
-
rescue Errno::ENOENT
-
create_logfile(filename)
-
end
-
end
-
-
1
def create_logfile(filename)
-
begin
-
logdev = File.open(filename, (File::WRONLY | File::APPEND | File::CREAT | File::EXCL))
-
logdev.flock(File::LOCK_EX)
-
logdev.sync = true
-
logdev.binmode if @binmode
-
add_log_header(logdev)
-
logdev.flock(File::LOCK_UN)
-
rescue Errno::EEXIST
-
# file is created by another process
-
logdev = open_logfile(filename)
-
logdev.sync = true
-
end
-
logdev
-
end
-
-
1
def add_log_header(file)
-
file.write(
-
"# Logfile created on %s by %s\n" % [Time.now.to_s, Logger::ProgName]
-
) if file.size == 0
-
end
-
-
1
def check_shift_log
-
if @shift_age.is_a?(Integer)
-
# Note: always returns false if '0'.
-
if @filename && (@shift_age > 0) && (@dev.stat.size > @shift_size)
-
lock_shift_log { shift_log_age }
-
end
-
else
-
now = Time.now
-
if now >= @next_rotate_time
-
@next_rotate_time = next_rotate_time(now, @shift_age)
-
lock_shift_log { shift_log_period(previous_period_end(now, @shift_age)) }
-
end
-
end
-
end
-
-
1
if /mswin|mingw/ =~ RUBY_PLATFORM
-
def lock_shift_log
-
yield
-
end
-
else
-
1
def lock_shift_log
-
retry_limit = 8
-
retry_sleep = 0.1
-
begin
-
File.open(@filename, File::WRONLY | File::APPEND) do |lock|
-
lock.flock(File::LOCK_EX) # inter-process locking. will be unlocked at closing file
-
if File.identical?(@filename, lock) and File.identical?(lock, @dev)
-
yield # log shifting
-
else
-
# log shifted by another process (i-node before locking and i-node after locking are different)
-
@dev.close rescue nil
-
@dev = open_logfile(@filename)
-
@dev.sync = true
-
end
-
end
-
rescue Errno::ENOENT
-
# @filename file would not exist right after #rename and before #create_logfile
-
if retry_limit <= 0
-
warn("log rotation inter-process lock failed. #{$!}")
-
else
-
sleep retry_sleep
-
retry_limit -= 1
-
retry_sleep *= 2
-
retry
-
end
-
end
-
rescue
-
warn("log rotation inter-process lock failed. #{$!}")
-
end
-
end
-
-
1
def shift_log_age
-
(@shift_age-3).downto(0) do |i|
-
if FileTest.exist?("#{@filename}.#{i}")
-
File.rename("#{@filename}.#{i}", "#{@filename}.#{i+1}")
-
end
-
end
-
@dev.close rescue nil
-
File.rename("#{@filename}", "#{@filename}.0")
-
@dev = create_logfile(@filename)
-
return true
-
end
-
-
1
def shift_log_period(period_end)
-
suffix = period_end.strftime(@shift_period_suffix)
-
age_file = "#{@filename}.#{suffix}"
-
if FileTest.exist?(age_file)
-
# try to avoid filename crash caused by Timestamp change.
-
idx = 0
-
# .99 can be overridden; avoid too much file search with 'loop do'
-
while idx < 100
-
idx += 1
-
age_file = "#{@filename}.#{suffix}.#{idx}"
-
break unless FileTest.exist?(age_file)
-
end
-
end
-
@dev.close rescue nil
-
File.rename("#{@filename}", age_file)
-
@dev = create_logfile(@filename)
-
return true
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Logger
-
1
module Period
-
1
module_function
-
-
1
SiD = 24 * 60 * 60
-
-
1
def next_rotate_time(now, shift_age)
-
case shift_age
-
when 'daily'
-
t = Time.mktime(now.year, now.month, now.mday) + SiD
-
when 'weekly'
-
t = Time.mktime(now.year, now.month, now.mday) + SiD * (7 - now.wday)
-
when 'monthly'
-
t = Time.mktime(now.year, now.month, 1) + SiD * 32
-
return Time.mktime(t.year, t.month, 1)
-
when 'now', 'everytime'
-
return now
-
else
-
raise ArgumentError, "invalid :shift_age #{shift_age.inspect}, should be daily, weekly, monthly, or everytime"
-
end
-
if t.hour.nonzero? or t.min.nonzero? or t.sec.nonzero?
-
hour = t.hour
-
t = Time.mktime(t.year, t.month, t.mday)
-
t += SiD if hour > 12
-
end
-
t
-
end
-
-
1
def previous_period_end(now, shift_age)
-
case shift_age
-
when 'daily'
-
t = Time.mktime(now.year, now.month, now.mday) - SiD / 2
-
when 'weekly'
-
t = Time.mktime(now.year, now.month, now.mday) - (SiD * now.wday + SiD / 2)
-
when 'monthly'
-
t = Time.mktime(now.year, now.month, 1) - SiD / 2
-
when 'now', 'everytime'
-
return now
-
else
-
raise ArgumentError, "invalid :shift_age #{shift_age.inspect}, should be daily, weekly, monthly, or everytime"
-
end
-
Time.mktime(t.year, t.month, t.mday, 23, 59, 59)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Logger
-
# Logging severity.
-
1
module Severity
-
# Low-level information, mostly for developers.
-
1
DEBUG = 0
-
# Generic (useful) information about system operation.
-
1
INFO = 1
-
# A warning.
-
1
WARN = 2
-
# A handleable error condition.
-
1
ERROR = 3
-
# An unhandleable error that results in a program crash.
-
1
FATAL = 4
-
# An unknown message that should always be logged.
-
1
UNKNOWN = 5
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class Logger
-
1
VERSION = "1.4.2"
-
end
-
# frozen_string_literal: false
-
#
-
# = net/http.rb
-
#
-
# Copyright (c) 1999-2007 Yukihiro Matsumoto
-
# Copyright (c) 1999-2007 Minero Aoki
-
# Copyright (c) 2001 GOTOU Yuuzou
-
#
-
# Written and maintained by Minero Aoki <aamine@loveruby.net>.
-
# HTTPS support added by GOTOU Yuuzou <gotoyuzo@notwork.org>.
-
#
-
# This file is derived from "http-access.rb".
-
#
-
# Documented by Minero Aoki; converted to RDoc by William Webber.
-
#
-
# This program is free software. You can re-distribute and/or
-
# modify this program under the same terms of ruby itself ---
-
# Ruby Distribution License or GNU General Public License.
-
#
-
# See Net::HTTP for an overview and examples.
-
#
-
-
1
require_relative 'protocol'
-
1
require 'uri'
-
1
autoload :OpenSSL, 'openssl'
-
-
1
module Net #:nodoc:
-
-
# :stopdoc:
-
1
class HTTPBadResponse < StandardError; end
-
1
class HTTPHeaderSyntaxError < StandardError; end
-
# :startdoc:
-
-
# == An HTTP client API for Ruby.
-
#
-
# Net::HTTP provides a rich library which can be used to build HTTP
-
# user-agents. For more details about HTTP see
-
# [RFC2616](http://www.ietf.org/rfc/rfc2616.txt).
-
#
-
# Net::HTTP is designed to work closely with URI. URI::HTTP#host,
-
# URI::HTTP#port and URI::HTTP#request_uri are designed to work with
-
# Net::HTTP.
-
#
-
# If you are only performing a few GET requests you should try OpenURI.
-
#
-
# == Simple Examples
-
#
-
# All examples assume you have loaded Net::HTTP with:
-
#
-
# require 'net/http'
-
#
-
# This will also require 'uri' so you don't need to require it separately.
-
#
-
# The Net::HTTP methods in the following section do not persist
-
# connections. They are not recommended if you are performing many HTTP
-
# requests.
-
#
-
# === GET
-
#
-
# Net::HTTP.get('example.com', '/index.html') # => String
-
#
-
# === GET by URI
-
#
-
# uri = URI('http://example.com/index.html?count=10')
-
# Net::HTTP.get(uri) # => String
-
#
-
# === GET with Dynamic Parameters
-
#
-
# uri = URI('http://example.com/index.html')
-
# params = { :limit => 10, :page => 3 }
-
# uri.query = URI.encode_www_form(params)
-
#
-
# res = Net::HTTP.get_response(uri)
-
# puts res.body if res.is_a?(Net::HTTPSuccess)
-
#
-
# === POST
-
#
-
# uri = URI('http://www.example.com/search.cgi')
-
# res = Net::HTTP.post_form(uri, 'q' => 'ruby', 'max' => '50')
-
# puts res.body
-
#
-
# === POST with Multiple Values
-
#
-
# uri = URI('http://www.example.com/search.cgi')
-
# res = Net::HTTP.post_form(uri, 'q' => ['ruby', 'perl'], 'max' => '50')
-
# puts res.body
-
#
-
# == How to use Net::HTTP
-
#
-
# The following example code can be used as the basis of an HTTP user-agent
-
# which can perform a variety of request types using persistent
-
# connections.
-
#
-
# uri = URI('http://example.com/some_path?query=string')
-
#
-
# Net::HTTP.start(uri.host, uri.port) do |http|
-
# request = Net::HTTP::Get.new uri
-
#
-
# response = http.request request # Net::HTTPResponse object
-
# end
-
#
-
# Net::HTTP::start immediately creates a connection to an HTTP server which
-
# is kept open for the duration of the block. The connection will remain
-
# open for multiple requests in the block if the server indicates it
-
# supports persistent connections.
-
#
-
# If you wish to re-use a connection across multiple HTTP requests without
-
# automatically closing it you can use ::new and then call #start and
-
# #finish manually.
-
#
-
# The request types Net::HTTP supports are listed below in the section "HTTP
-
# Request Classes".
-
#
-
# For all the Net::HTTP request objects and shortcut request methods you may
-
# supply either a String for the request path or a URI from which Net::HTTP
-
# will extract the request path.
-
#
-
# === Response Data
-
#
-
# uri = URI('http://example.com/index.html')
-
# res = Net::HTTP.get_response(uri)
-
#
-
# # Headers
-
# res['Set-Cookie'] # => String
-
# res.get_fields('set-cookie') # => Array
-
# res.to_hash['set-cookie'] # => Array
-
# puts "Headers: #{res.to_hash.inspect}"
-
#
-
# # Status
-
# puts res.code # => '200'
-
# puts res.message # => 'OK'
-
# puts res.class.name # => 'HTTPOK'
-
#
-
# # Body
-
# puts res.body if res.response_body_permitted?
-
#
-
# === Following Redirection
-
#
-
# Each Net::HTTPResponse object belongs to a class for its response code.
-
#
-
# For example, all 2XX responses are instances of a Net::HTTPSuccess
-
# subclass, a 3XX response is an instance of a Net::HTTPRedirection
-
# subclass and a 200 response is an instance of the Net::HTTPOK class. For
-
# details of response classes, see the section "HTTP Response Classes"
-
# below.
-
#
-
# Using a case statement you can handle various types of responses properly:
-
#
-
# def fetch(uri_str, limit = 10)
-
# # You should choose a better exception.
-
# raise ArgumentError, 'too many HTTP redirects' if limit == 0
-
#
-
# response = Net::HTTP.get_response(URI(uri_str))
-
#
-
# case response
-
# when Net::HTTPSuccess then
-
# response
-
# when Net::HTTPRedirection then
-
# location = response['location']
-
# warn "redirected to #{location}"
-
# fetch(location, limit - 1)
-
# else
-
# response.value
-
# end
-
# end
-
#
-
# print fetch('http://www.ruby-lang.org')
-
#
-
# === POST
-
#
-
# A POST can be made using the Net::HTTP::Post request class. This example
-
# creates a URL encoded POST body:
-
#
-
# uri = URI('http://www.example.com/todo.cgi')
-
# req = Net::HTTP::Post.new(uri)
-
# req.set_form_data('from' => '2005-01-01', 'to' => '2005-03-31')
-
#
-
# res = Net::HTTP.start(uri.hostname, uri.port) do |http|
-
# http.request(req)
-
# end
-
#
-
# case res
-
# when Net::HTTPSuccess, Net::HTTPRedirection
-
# # OK
-
# else
-
# res.value
-
# end
-
#
-
# To send multipart/form-data use Net::HTTPHeader#set_form:
-
#
-
# req = Net::HTTP::Post.new(uri)
-
# req.set_form([['upload', File.open('foo.bar')]], 'multipart/form-data')
-
#
-
# Other requests that can contain a body such as PUT can be created in the
-
# same way using the corresponding request class (Net::HTTP::Put).
-
#
-
# === Setting Headers
-
#
-
# The following example performs a conditional GET using the
-
# If-Modified-Since header. If the files has not been modified since the
-
# time in the header a Not Modified response will be returned. See RFC 2616
-
# section 9.3 for further details.
-
#
-
# uri = URI('http://example.com/cached_response')
-
# file = File.stat 'cached_response'
-
#
-
# req = Net::HTTP::Get.new(uri)
-
# req['If-Modified-Since'] = file.mtime.rfc2822
-
#
-
# res = Net::HTTP.start(uri.hostname, uri.port) {|http|
-
# http.request(req)
-
# }
-
#
-
# open 'cached_response', 'w' do |io|
-
# io.write res.body
-
# end if res.is_a?(Net::HTTPSuccess)
-
#
-
# === Basic Authentication
-
#
-
# Basic authentication is performed according to
-
# [RFC2617](http://www.ietf.org/rfc/rfc2617.txt).
-
#
-
# uri = URI('http://example.com/index.html?key=value')
-
#
-
# req = Net::HTTP::Get.new(uri)
-
# req.basic_auth 'user', 'pass'
-
#
-
# res = Net::HTTP.start(uri.hostname, uri.port) {|http|
-
# http.request(req)
-
# }
-
# puts res.body
-
#
-
# === Streaming Response Bodies
-
#
-
# By default Net::HTTP reads an entire response into memory. If you are
-
# handling large files or wish to implement a progress bar you can instead
-
# stream the body directly to an IO.
-
#
-
# uri = URI('http://example.com/large_file')
-
#
-
# Net::HTTP.start(uri.host, uri.port) do |http|
-
# request = Net::HTTP::Get.new uri
-
#
-
# http.request request do |response|
-
# open 'large_file', 'w' do |io|
-
# response.read_body do |chunk|
-
# io.write chunk
-
# end
-
# end
-
# end
-
# end
-
#
-
# === HTTPS
-
#
-
# HTTPS is enabled for an HTTP connection by Net::HTTP#use_ssl=.
-
#
-
# uri = URI('https://secure.example.com/some_path?query=string')
-
#
-
# Net::HTTP.start(uri.host, uri.port, :use_ssl => true) do |http|
-
# request = Net::HTTP::Get.new uri
-
# response = http.request request # Net::HTTPResponse object
-
# end
-
#
-
# Or if you simply want to make a GET request, you may pass in an URI
-
# object that has an HTTPS URL. Net::HTTP automatically turns on TLS
-
# verification if the URI object has a 'https' URI scheme.
-
#
-
# uri = URI('https://example.com/')
-
# Net::HTTP.get(uri) # => String
-
#
-
# In previous versions of Ruby you would need to require 'net/https' to use
-
# HTTPS. This is no longer true.
-
#
-
# === Proxies
-
#
-
# Net::HTTP will automatically create a proxy from the +http_proxy+
-
# environment variable if it is present. To disable use of +http_proxy+,
-
# pass +nil+ for the proxy address.
-
#
-
# You may also create a custom proxy:
-
#
-
# proxy_addr = 'your.proxy.host'
-
# proxy_port = 8080
-
#
-
# Net::HTTP.new('example.com', nil, proxy_addr, proxy_port).start { |http|
-
# # always proxy via your.proxy.addr:8080
-
# }
-
#
-
# See Net::HTTP.new for further details and examples such as proxies that
-
# require a username and password.
-
#
-
# === Compression
-
#
-
# Net::HTTP automatically adds Accept-Encoding for compression of response
-
# bodies and automatically decompresses gzip and deflate responses unless a
-
# Range header was sent.
-
#
-
# Compression can be disabled through the Accept-Encoding: identity header.
-
#
-
# == HTTP Request Classes
-
#
-
# Here is the HTTP request class hierarchy.
-
#
-
# * Net::HTTPRequest
-
# * Net::HTTP::Get
-
# * Net::HTTP::Head
-
# * Net::HTTP::Post
-
# * Net::HTTP::Patch
-
# * Net::HTTP::Put
-
# * Net::HTTP::Proppatch
-
# * Net::HTTP::Lock
-
# * Net::HTTP::Unlock
-
# * Net::HTTP::Options
-
# * Net::HTTP::Propfind
-
# * Net::HTTP::Delete
-
# * Net::HTTP::Move
-
# * Net::HTTP::Copy
-
# * Net::HTTP::Mkcol
-
# * Net::HTTP::Trace
-
#
-
# == HTTP Response Classes
-
#
-
# Here is HTTP response class hierarchy. All classes are defined in Net
-
# module and are subclasses of Net::HTTPResponse.
-
#
-
# HTTPUnknownResponse:: For unhandled HTTP extensions
-
# HTTPInformation:: 1xx
-
# HTTPContinue:: 100
-
# HTTPSwitchProtocol:: 101
-
# HTTPSuccess:: 2xx
-
# HTTPOK:: 200
-
# HTTPCreated:: 201
-
# HTTPAccepted:: 202
-
# HTTPNonAuthoritativeInformation:: 203
-
# HTTPNoContent:: 204
-
# HTTPResetContent:: 205
-
# HTTPPartialContent:: 206
-
# HTTPMultiStatus:: 207
-
# HTTPIMUsed:: 226
-
# HTTPRedirection:: 3xx
-
# HTTPMultipleChoices:: 300
-
# HTTPMovedPermanently:: 301
-
# HTTPFound:: 302
-
# HTTPSeeOther:: 303
-
# HTTPNotModified:: 304
-
# HTTPUseProxy:: 305
-
# HTTPTemporaryRedirect:: 307
-
# HTTPClientError:: 4xx
-
# HTTPBadRequest:: 400
-
# HTTPUnauthorized:: 401
-
# HTTPPaymentRequired:: 402
-
# HTTPForbidden:: 403
-
# HTTPNotFound:: 404
-
# HTTPMethodNotAllowed:: 405
-
# HTTPNotAcceptable:: 406
-
# HTTPProxyAuthenticationRequired:: 407
-
# HTTPRequestTimeOut:: 408
-
# HTTPConflict:: 409
-
# HTTPGone:: 410
-
# HTTPLengthRequired:: 411
-
# HTTPPreconditionFailed:: 412
-
# HTTPRequestEntityTooLarge:: 413
-
# HTTPRequestURITooLong:: 414
-
# HTTPUnsupportedMediaType:: 415
-
# HTTPRequestedRangeNotSatisfiable:: 416
-
# HTTPExpectationFailed:: 417
-
# HTTPUnprocessableEntity:: 422
-
# HTTPLocked:: 423
-
# HTTPFailedDependency:: 424
-
# HTTPUpgradeRequired:: 426
-
# HTTPPreconditionRequired:: 428
-
# HTTPTooManyRequests:: 429
-
# HTTPRequestHeaderFieldsTooLarge:: 431
-
# HTTPUnavailableForLegalReasons:: 451
-
# HTTPServerError:: 5xx
-
# HTTPInternalServerError:: 500
-
# HTTPNotImplemented:: 501
-
# HTTPBadGateway:: 502
-
# HTTPServiceUnavailable:: 503
-
# HTTPGatewayTimeOut:: 504
-
# HTTPVersionNotSupported:: 505
-
# HTTPInsufficientStorage:: 507
-
# HTTPNetworkAuthenticationRequired:: 511
-
#
-
# There is also the Net::HTTPBadResponse exception which is raised when
-
# there is a protocol error.
-
#
-
1
class HTTP < Protocol
-
-
# :stopdoc:
-
1
Revision = %q$Revision$.split[1]
-
1
HTTPVersion = '1.1'
-
begin
-
1
require 'zlib'
-
1
require 'stringio' #for our purposes (unpacking gzip) lump these together
-
1
HAVE_ZLIB=true
-
rescue LoadError
-
HAVE_ZLIB=false
-
end
-
# :startdoc:
-
-
# Turns on net/http 1.2 (Ruby 1.8) features.
-
# Defaults to ON in Ruby 1.8 or later.
-
1
def HTTP.version_1_2
-
true
-
end
-
-
# Returns true if net/http is in version 1.2 mode.
-
# Defaults to true.
-
1
def HTTP.version_1_2?
-
true
-
end
-
-
1
def HTTP.version_1_1? #:nodoc:
-
false
-
end
-
-
1
class << HTTP
-
1
alias is_version_1_1? version_1_1? #:nodoc:
-
1
alias is_version_1_2? version_1_2? #:nodoc:
-
end
-
-
#
-
# short cut methods
-
#
-
-
#
-
# Gets the body text from the target and outputs it to $stdout. The
-
# target can either be specified as
-
# (+uri+), or as (+host+, +path+, +port+ = 80); so:
-
#
-
# Net::HTTP.get_print URI('http://www.example.com/index.html')
-
#
-
# or:
-
#
-
# Net::HTTP.get_print 'www.example.com', '/index.html'
-
#
-
1
def HTTP.get_print(uri_or_host, path = nil, port = nil)
-
get_response(uri_or_host, path, port) {|res|
-
res.read_body do |chunk|
-
$stdout.print chunk
-
end
-
}
-
nil
-
end
-
-
# Sends a GET request to the target and returns the HTTP response
-
# as a string. The target can either be specified as
-
# (+uri+), or as (+host+, +path+, +port+ = 80); so:
-
#
-
# print Net::HTTP.get(URI('http://www.example.com/index.html'))
-
#
-
# or:
-
#
-
# print Net::HTTP.get('www.example.com', '/index.html')
-
#
-
1
def HTTP.get(uri_or_host, path = nil, port = nil)
-
get_response(uri_or_host, path, port).body
-
end
-
-
# Sends a GET request to the target and returns the HTTP response
-
# as a Net::HTTPResponse object. The target can either be specified as
-
# (+uri+), or as (+host+, +path+, +port+ = 80); so:
-
#
-
# res = Net::HTTP.get_response(URI('http://www.example.com/index.html'))
-
# print res.body
-
#
-
# or:
-
#
-
# res = Net::HTTP.get_response('www.example.com', '/index.html')
-
# print res.body
-
#
-
1
def HTTP.get_response(uri_or_host, path = nil, port = nil, &block)
-
if path
-
host = uri_or_host
-
new(host, port || HTTP.default_port).start {|http|
-
return http.request_get(path, &block)
-
}
-
else
-
uri = uri_or_host
-
start(uri.hostname, uri.port,
-
:use_ssl => uri.scheme == 'https') {|http|
-
return http.request_get(uri, &block)
-
}
-
end
-
end
-
-
# Posts data to the specified URI object.
-
#
-
# Example:
-
#
-
# require 'net/http'
-
# require 'uri'
-
#
-
# Net::HTTP.post URI('http://www.example.com/api/search'),
-
# { "q" => "ruby", "max" => "50" }.to_json,
-
# "Content-Type" => "application/json"
-
#
-
1
def HTTP.post(url, data, header = nil)
-
start(url.hostname, url.port,
-
:use_ssl => url.scheme == 'https' ) {|http|
-
http.post(url, data, header)
-
}
-
end
-
-
# Posts HTML form data to the specified URI object.
-
# The form data must be provided as a Hash mapping from String to String.
-
# Example:
-
#
-
# { "cmd" => "search", "q" => "ruby", "max" => "50" }
-
#
-
# This method also does Basic Authentication iff +url+.user exists.
-
# But userinfo for authentication is deprecated (RFC3986).
-
# So this feature will be removed.
-
#
-
# Example:
-
#
-
# require 'net/http'
-
# require 'uri'
-
#
-
# Net::HTTP.post_form URI('http://www.example.com/search.cgi'),
-
# { "q" => "ruby", "max" => "50" }
-
#
-
1
def HTTP.post_form(url, params)
-
req = Post.new(url)
-
req.form_data = params
-
req.basic_auth url.user, url.password if url.user
-
start(url.hostname, url.port,
-
:use_ssl => url.scheme == 'https' ) {|http|
-
http.request(req)
-
}
-
end
-
-
#
-
# HTTP session management
-
#
-
-
# The default port to use for HTTP requests; defaults to 80.
-
1
def HTTP.default_port
-
http_default_port()
-
end
-
-
# The default port to use for HTTP requests; defaults to 80.
-
1
def HTTP.http_default_port
-
80
-
end
-
-
# The default port to use for HTTPS requests; defaults to 443.
-
1
def HTTP.https_default_port
-
443
-
end
-
-
1
def HTTP.socket_type #:nodoc: obsolete
-
BufferedIO
-
end
-
-
# :call-seq:
-
# HTTP.start(address, port, p_addr, p_port, p_user, p_pass, &block)
-
# HTTP.start(address, port=nil, p_addr=:ENV, p_port=nil, p_user=nil, p_pass=nil, opt, &block)
-
#
-
# Creates a new Net::HTTP object, then additionally opens the TCP
-
# connection and HTTP session.
-
#
-
# Arguments are the following:
-
# _address_ :: hostname or IP address of the server
-
# _port_ :: port of the server
-
# _p_addr_ :: address of proxy
-
# _p_port_ :: port of proxy
-
# _p_user_ :: user of proxy
-
# _p_pass_ :: pass of proxy
-
# _opt_ :: optional hash
-
#
-
# _opt_ sets following values by its accessor.
-
# The keys are ipaddr, ca_file, ca_path, cert, cert_store, ciphers,
-
# close_on_empty_response, key, open_timeout, read_timeout, write_timeout, ssl_timeout,
-
# ssl_version, use_ssl, verify_callback, verify_depth and verify_mode.
-
# If you set :use_ssl as true, you can use https and default value of
-
# verify_mode is set as OpenSSL::SSL::VERIFY_PEER.
-
#
-
# If the optional block is given, the newly
-
# created Net::HTTP object is passed to it and closed when the
-
# block finishes. In this case, the return value of this method
-
# is the return value of the block. If no block is given, the
-
# return value of this method is the newly created Net::HTTP object
-
# itself, and the caller is responsible for closing it upon completion
-
# using the finish() method.
-
1
def HTTP.start(address, *arg, &block) # :yield: +http+
-
arg.pop if opt = Hash.try_convert(arg[-1])
-
port, p_addr, p_port, p_user, p_pass = *arg
-
p_addr = :ENV if arg.size < 2
-
port = https_default_port if !port && opt && opt[:use_ssl]
-
http = new(address, port, p_addr, p_port, p_user, p_pass)
-
http.ipaddr = opt[:ipaddr] if opt && opt[:ipaddr]
-
-
if opt
-
if opt[:use_ssl]
-
opt = {verify_mode: OpenSSL::SSL::VERIFY_PEER}.update(opt)
-
end
-
http.methods.grep(/\A(\w+)=\z/) do |meth|
-
key = $1.to_sym
-
opt.key?(key) or next
-
http.__send__(meth, opt[key])
-
end
-
end
-
-
http.start(&block)
-
end
-
-
1
class << HTTP
-
1
alias newobj new # :nodoc:
-
end
-
-
# Creates a new Net::HTTP object without opening a TCP connection or
-
# HTTP session.
-
#
-
# The +address+ should be a DNS hostname or IP address, the +port+ is the
-
# port the server operates on. If no +port+ is given the default port for
-
# HTTP or HTTPS is used.
-
#
-
# If none of the +p_+ arguments are given, the proxy host and port are
-
# taken from the +http_proxy+ environment variable (or its uppercase
-
# equivalent) if present. If the proxy requires authentication you must
-
# supply it by hand. See URI::Generic#find_proxy for details of proxy
-
# detection from the environment. To disable proxy detection set +p_addr+
-
# to nil.
-
#
-
# If you are connecting to a custom proxy, +p_addr+ specifies the DNS name
-
# or IP address of the proxy host, +p_port+ the port to use to access the
-
# proxy, +p_user+ and +p_pass+ the username and password if authorization
-
# is required to use the proxy, and p_no_proxy hosts which do not
-
# use the proxy.
-
#
-
1
def HTTP.new(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_no_proxy = nil)
-
http = super address, port
-
-
if proxy_class? then # from Net::HTTP::Proxy()
-
http.proxy_from_env = @proxy_from_env
-
http.proxy_address = @proxy_address
-
http.proxy_port = @proxy_port
-
http.proxy_user = @proxy_user
-
http.proxy_pass = @proxy_pass
-
elsif p_addr == :ENV then
-
http.proxy_from_env = true
-
else
-
if p_addr && p_no_proxy && !URI::Generic.use_proxy?(p_addr, p_addr, p_port, p_no_proxy)
-
p_addr = nil
-
p_port = nil
-
end
-
http.proxy_address = p_addr
-
http.proxy_port = p_port || default_port
-
http.proxy_user = p_user
-
http.proxy_pass = p_pass
-
end
-
-
http
-
end
-
-
# Creates a new Net::HTTP object for the specified server address,
-
# without opening the TCP connection or initializing the HTTP session.
-
# The +address+ should be a DNS hostname or IP address.
-
1
def initialize(address, port = nil)
-
@address = address
-
@port = (port || HTTP.default_port)
-
@ipaddr = nil
-
@local_host = nil
-
@local_port = nil
-
@curr_http_version = HTTPVersion
-
@keep_alive_timeout = 2
-
@last_communicated = nil
-
@close_on_empty_response = false
-
@socket = nil
-
@started = false
-
@open_timeout = 60
-
@read_timeout = 60
-
@write_timeout = 60
-
@continue_timeout = nil
-
@max_retries = 1
-
@debug_output = nil
-
-
@proxy_from_env = false
-
@proxy_uri = nil
-
@proxy_address = nil
-
@proxy_port = nil
-
@proxy_user = nil
-
@proxy_pass = nil
-
-
@use_ssl = false
-
@ssl_context = nil
-
@ssl_session = nil
-
@sspi_enabled = false
-
SSL_IVNAMES.each do |ivname|
-
instance_variable_set ivname, nil
-
end
-
end
-
-
1
def inspect
-
"#<#{self.class} #{@address}:#{@port} open=#{started?}>"
-
end
-
-
# *WARNING* This method opens a serious security hole.
-
# Never use this method in production code.
-
#
-
# Sets an output stream for debugging.
-
#
-
# http = Net::HTTP.new(hostname)
-
# http.set_debug_output $stderr
-
# http.start { .... }
-
#
-
1
def set_debug_output(output)
-
warn 'Net::HTTP#set_debug_output called after HTTP started', uplevel: 1 if started?
-
@debug_output = output
-
end
-
-
# The DNS host name or IP address to connect to.
-
1
attr_reader :address
-
-
# The port number to connect to.
-
1
attr_reader :port
-
-
# The local host used to establish the connection.
-
1
attr_accessor :local_host
-
-
# The local port used to establish the connection.
-
1
attr_accessor :local_port
-
-
1
attr_writer :proxy_from_env
-
1
attr_writer :proxy_address
-
1
attr_writer :proxy_port
-
1
attr_writer :proxy_user
-
1
attr_writer :proxy_pass
-
-
# The IP address to connect to/used to connect to
-
1
def ipaddr
-
started? ? @socket.io.peeraddr[3] : @ipaddr
-
end
-
-
# Set the IP address to connect to
-
1
def ipaddr=(addr)
-
raise IOError, "ipaddr value changed, but session already started" if started?
-
@ipaddr = addr
-
end
-
-
# Number of seconds to wait for the connection to open. Any number
-
# may be used, including Floats for fractional seconds. If the HTTP
-
# object cannot open a connection in this many seconds, it raises a
-
# Net::OpenTimeout exception. The default value is 60 seconds.
-
1
attr_accessor :open_timeout
-
-
# Number of seconds to wait for one block to be read (via one read(2)
-
# call). Any number may be used, including Floats for fractional
-
# seconds. If the HTTP object cannot read data in this many seconds,
-
# it raises a Net::ReadTimeout exception. The default value is 60 seconds.
-
1
attr_reader :read_timeout
-
-
# Number of seconds to wait for one block to be written (via one write(2)
-
# call). Any number may be used, including Floats for fractional
-
# seconds. If the HTTP object cannot write data in this many seconds,
-
# it raises a Net::WriteTimeout exception. The default value is 60 seconds.
-
# Net::WriteTimeout is not raised on Windows.
-
1
attr_reader :write_timeout
-
-
# Maximum number of times to retry an idempotent request in case of
-
# Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET,
-
# Errno::ECONNABORTED, Errno::EPIPE, OpenSSL::SSL::SSLError,
-
# Timeout::Error.
-
# Should be a non-negative integer number. Zero means no retries.
-
# The default value is 1.
-
1
def max_retries=(retries)
-
retries = retries.to_int
-
if retries < 0
-
raise ArgumentError, 'max_retries should be non-negative integer number'
-
end
-
@max_retries = retries
-
end
-
-
1
attr_reader :max_retries
-
-
# Setter for the read_timeout attribute.
-
1
def read_timeout=(sec)
-
@socket.read_timeout = sec if @socket
-
@read_timeout = sec
-
end
-
-
# Setter for the write_timeout attribute.
-
1
def write_timeout=(sec)
-
@socket.write_timeout = sec if @socket
-
@write_timeout = sec
-
end
-
-
# Seconds to wait for 100 Continue response. If the HTTP object does not
-
# receive a response in this many seconds it sends the request body. The
-
# default value is +nil+.
-
1
attr_reader :continue_timeout
-
-
# Setter for the continue_timeout attribute.
-
1
def continue_timeout=(sec)
-
@socket.continue_timeout = sec if @socket
-
@continue_timeout = sec
-
end
-
-
# Seconds to reuse the connection of the previous request.
-
# If the idle time is less than this Keep-Alive Timeout,
-
# Net::HTTP reuses the TCP/IP socket used by the previous communication.
-
# The default value is 2 seconds.
-
1
attr_accessor :keep_alive_timeout
-
-
# Returns true if the HTTP session has been started.
-
1
def started?
-
@started
-
end
-
-
1
alias active? started? #:nodoc: obsolete
-
-
1
attr_accessor :close_on_empty_response
-
-
# Returns true if SSL/TLS is being used with HTTP.
-
1
def use_ssl?
-
@use_ssl
-
end
-
-
# Turn on/off SSL.
-
# This flag must be set before starting session.
-
# If you change use_ssl value after session started,
-
# a Net::HTTP object raises IOError.
-
1
def use_ssl=(flag)
-
flag = flag ? true : false
-
if started? and @use_ssl != flag
-
raise IOError, "use_ssl value changed, but session already started"
-
end
-
@use_ssl = flag
-
end
-
-
1
SSL_IVNAMES = [
-
:@ca_file,
-
:@ca_path,
-
:@cert,
-
:@cert_store,
-
:@ciphers,
-
:@key,
-
:@ssl_timeout,
-
:@ssl_version,
-
:@min_version,
-
:@max_version,
-
:@verify_callback,
-
:@verify_depth,
-
:@verify_mode,
-
]
-
1
SSL_ATTRIBUTES = [
-
:ca_file,
-
:ca_path,
-
:cert,
-
:cert_store,
-
:ciphers,
-
:key,
-
:ssl_timeout,
-
:ssl_version,
-
:min_version,
-
:max_version,
-
:verify_callback,
-
:verify_depth,
-
:verify_mode,
-
]
-
-
# Sets path of a CA certification file in PEM format.
-
#
-
# The file can contain several CA certificates.
-
1
attr_accessor :ca_file
-
-
# Sets path of a CA certification directory containing certifications in
-
# PEM format.
-
1
attr_accessor :ca_path
-
-
# Sets an OpenSSL::X509::Certificate object as client certificate.
-
# (This method is appeared in Michal Rokos's OpenSSL extension).
-
1
attr_accessor :cert
-
-
# Sets the X509::Store to verify peer certificate.
-
1
attr_accessor :cert_store
-
-
# Sets the available ciphers. See OpenSSL::SSL::SSLContext#ciphers=
-
1
attr_accessor :ciphers
-
-
# Sets an OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
-
# (This method is appeared in Michal Rokos's OpenSSL extension.)
-
1
attr_accessor :key
-
-
# Sets the SSL timeout seconds.
-
1
attr_accessor :ssl_timeout
-
-
# Sets the SSL version. See OpenSSL::SSL::SSLContext#ssl_version=
-
1
attr_accessor :ssl_version
-
-
# Sets the minimum SSL version. See OpenSSL::SSL::SSLContext#min_version=
-
1
attr_accessor :min_version
-
-
# Sets the maximum SSL version. See OpenSSL::SSL::SSLContext#max_version=
-
1
attr_accessor :max_version
-
-
# Sets the verify callback for the server certification verification.
-
1
attr_accessor :verify_callback
-
-
# Sets the maximum depth for the certificate chain verification.
-
1
attr_accessor :verify_depth
-
-
# Sets the flags for server the certification verification at beginning of
-
# SSL/TLS session.
-
#
-
# OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER are acceptable.
-
1
attr_accessor :verify_mode
-
-
# Returns the X.509 certificates the server presented.
-
1
def peer_cert
-
if not use_ssl? or not @socket
-
return nil
-
end
-
@socket.io.peer_cert
-
end
-
-
# Opens a TCP connection and HTTP session.
-
#
-
# When this method is called with a block, it passes the Net::HTTP
-
# object to the block, and closes the TCP connection and HTTP session
-
# after the block has been executed.
-
#
-
# When called with a block, it returns the return value of the
-
# block; otherwise, it returns self.
-
#
-
1
def start # :yield: http
-
raise IOError, 'HTTP session already opened' if @started
-
if block_given?
-
begin
-
do_start
-
return yield(self)
-
ensure
-
do_finish
-
end
-
end
-
do_start
-
self
-
end
-
-
1
def do_start
-
connect
-
@started = true
-
end
-
1
private :do_start
-
-
1
def connect
-
if proxy? then
-
conn_addr = proxy_address
-
conn_port = proxy_port
-
else
-
conn_addr = conn_address
-
conn_port = port
-
end
-
-
D "opening connection to #{conn_addr}:#{conn_port}..."
-
s = Timeout.timeout(@open_timeout, Net::OpenTimeout) {
-
begin
-
TCPSocket.open(conn_addr, conn_port, @local_host, @local_port)
-
rescue => e
-
raise e, "Failed to open TCP connection to " +
-
"#{conn_addr}:#{conn_port} (#{e.message})"
-
end
-
}
-
s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
-
D "opened"
-
if use_ssl?
-
if proxy?
-
plain_sock = BufferedIO.new(s, read_timeout: @read_timeout,
-
write_timeout: @write_timeout,
-
continue_timeout: @continue_timeout,
-
debug_output: @debug_output)
-
buf = "CONNECT #{conn_address}:#{@port} HTTP/#{HTTPVersion}\r\n"
-
buf << "Host: #{@address}:#{@port}\r\n"
-
if proxy_user
-
credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0')
-
buf << "Proxy-Authorization: Basic #{credential}\r\n"
-
end
-
buf << "\r\n"
-
plain_sock.write(buf)
-
HTTPResponse.read_new(plain_sock).value
-
# assuming nothing left in buffers after successful CONNECT response
-
end
-
-
ssl_parameters = Hash.new
-
iv_list = instance_variables
-
SSL_IVNAMES.each_with_index do |ivname, i|
-
if iv_list.include?(ivname) and
-
value = instance_variable_get(ivname)
-
ssl_parameters[SSL_ATTRIBUTES[i]] = value if value
-
end
-
end
-
@ssl_context = OpenSSL::SSL::SSLContext.new
-
@ssl_context.set_params(ssl_parameters)
-
@ssl_context.session_cache_mode =
-
OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
-
OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
-
@ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess }
-
D "starting SSL for #{conn_addr}:#{conn_port}..."
-
s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
-
s.sync_close = true
-
# Server Name Indication (SNI) RFC 3546
-
s.hostname = @address if s.respond_to? :hostname=
-
if @ssl_session and
-
Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
-
s.session = @ssl_session
-
end
-
ssl_socket_connect(s, @open_timeout)
-
if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
-
s.post_connection_check(@address)
-
end
-
D "SSL established, protocol: #{s.ssl_version}, cipher: #{s.cipher[0]}"
-
end
-
@socket = BufferedIO.new(s, read_timeout: @read_timeout,
-
write_timeout: @write_timeout,
-
continue_timeout: @continue_timeout,
-
debug_output: @debug_output)
-
on_connect
-
rescue => exception
-
if s
-
D "Conn close because of connect error #{exception}"
-
s.close
-
end
-
raise
-
end
-
1
private :connect
-
-
1
def on_connect
-
end
-
1
private :on_connect
-
-
# Finishes the HTTP session and closes the TCP connection.
-
# Raises IOError if the session has not been started.
-
1
def finish
-
raise IOError, 'HTTP session not yet started' unless started?
-
do_finish
-
end
-
-
1
def do_finish
-
@started = false
-
@socket.close if @socket
-
@socket = nil
-
end
-
1
private :do_finish
-
-
#
-
# proxy
-
#
-
-
1
public
-
-
# no proxy
-
1
@is_proxy_class = false
-
1
@proxy_from_env = false
-
1
@proxy_addr = nil
-
1
@proxy_port = nil
-
1
@proxy_user = nil
-
1
@proxy_pass = nil
-
-
# Creates an HTTP proxy class which behaves like Net::HTTP, but
-
# performs all access via the specified proxy.
-
#
-
# This class is obsolete. You may pass these same parameters directly to
-
# Net::HTTP.new. See Net::HTTP.new for details of the arguments.
-
1
def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil)
-
return self unless p_addr
-
-
Class.new(self) {
-
@is_proxy_class = true
-
-
if p_addr == :ENV then
-
@proxy_from_env = true
-
@proxy_address = nil
-
@proxy_port = nil
-
else
-
@proxy_from_env = false
-
@proxy_address = p_addr
-
@proxy_port = p_port || default_port
-
end
-
-
@proxy_user = p_user
-
@proxy_pass = p_pass
-
}
-
end
-
-
1
class << HTTP
-
# returns true if self is a class which was created by HTTP::Proxy.
-
1
def proxy_class?
-
defined?(@is_proxy_class) ? @is_proxy_class : false
-
end
-
-
# Address of proxy host. If Net::HTTP does not use a proxy, nil.
-
1
attr_reader :proxy_address
-
-
# Port number of proxy host. If Net::HTTP does not use a proxy, nil.
-
1
attr_reader :proxy_port
-
-
# User name for accessing proxy. If Net::HTTP does not use a proxy, nil.
-
1
attr_reader :proxy_user
-
-
# User password for accessing proxy. If Net::HTTP does not use a proxy,
-
# nil.
-
1
attr_reader :proxy_pass
-
end
-
-
# True if requests for this connection will be proxied
-
1
def proxy?
-
!!(@proxy_from_env ? proxy_uri : @proxy_address)
-
end
-
-
# True if the proxy for this connection is determined from the environment
-
1
def proxy_from_env?
-
@proxy_from_env
-
end
-
-
# The proxy URI determined from the environment for this connection.
-
1
def proxy_uri # :nodoc:
-
return if @proxy_uri == false
-
@proxy_uri ||= URI::HTTP.new(
-
"http".freeze, nil, address, port, nil, nil, nil, nil, nil
-
).find_proxy || false
-
@proxy_uri || nil
-
end
-
-
# The address of the proxy server, if one is configured.
-
1
def proxy_address
-
if @proxy_from_env then
-
proxy_uri&.hostname
-
else
-
@proxy_address
-
end
-
end
-
-
# The port of the proxy server, if one is configured.
-
1
def proxy_port
-
if @proxy_from_env then
-
proxy_uri&.port
-
else
-
@proxy_port
-
end
-
end
-
-
# [Bug #12921]
-
1
if /linux|freebsd|darwin/ =~ RUBY_PLATFORM
-
1
ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE = true
-
else
-
ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE = false
-
end
-
-
# The username of the proxy server, if one is configured.
-
1
def proxy_user
-
if ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE && @proxy_from_env
-
proxy_uri&.user
-
else
-
@proxy_user
-
end
-
end
-
-
# The password of the proxy server, if one is configured.
-
1
def proxy_pass
-
if ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE && @proxy_from_env
-
proxy_uri&.password
-
else
-
@proxy_pass
-
end
-
end
-
-
1
alias proxyaddr proxy_address #:nodoc: obsolete
-
1
alias proxyport proxy_port #:nodoc: obsolete
-
-
1
private
-
-
# without proxy, obsolete
-
-
1
def conn_address # :nodoc:
-
@ipaddr || address()
-
end
-
-
1
def conn_port # :nodoc:
-
port()
-
end
-
-
1
def edit_path(path)
-
if proxy?
-
if path.start_with?("ftp://") || use_ssl?
-
path
-
else
-
"http://#{addr_port}#{path}"
-
end
-
else
-
path
-
end
-
end
-
-
#
-
# HTTP operations
-
#
-
-
1
public
-
-
# Retrieves data from +path+ on the connected-to host which may be an
-
# absolute path String or a URI to extract the path from.
-
#
-
# +initheader+ must be a Hash like { 'Accept' => '*/*', ... },
-
# and it defaults to an empty hash.
-
# If +initheader+ doesn't have the key 'accept-encoding', then
-
# a value of "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" is used,
-
# so that gzip compression is used in preference to deflate
-
# compression, which is used in preference to no compression.
-
# Ruby doesn't have libraries to support the compress (Lempel-Ziv)
-
# compression, so that is not supported. The intent of this is
-
# to reduce bandwidth by default. If this routine sets up
-
# compression, then it does the decompression also, removing
-
# the header as well to prevent confusion. Otherwise
-
# it leaves the body as it found it.
-
#
-
# This method returns a Net::HTTPResponse object.
-
#
-
# If called with a block, yields each fragment of the
-
# entity body in turn as a string as it is read from
-
# the socket. Note that in this case, the returned response
-
# object will *not* contain a (meaningful) body.
-
#
-
# +dest+ argument is obsolete.
-
# It still works but you must not use it.
-
#
-
# This method never raises an exception.
-
#
-
# response = http.get('/index.html')
-
#
-
# # using block
-
# File.open('result.txt', 'w') {|f|
-
# http.get('/~foo/') do |str|
-
# f.write str
-
# end
-
# }
-
#
-
1
def get(path, initheader = nil, dest = nil, &block) # :yield: +body_segment+
-
res = nil
-
request(Get.new(path, initheader)) {|r|
-
r.read_body dest, &block
-
res = r
-
}
-
res
-
end
-
-
# Gets only the header from +path+ on the connected-to host.
-
# +header+ is a Hash like { 'Accept' => '*/*', ... }.
-
#
-
# This method returns a Net::HTTPResponse object.
-
#
-
# This method never raises an exception.
-
#
-
# response = nil
-
# Net::HTTP.start('some.www.server', 80) {|http|
-
# response = http.head('/index.html')
-
# }
-
# p response['content-type']
-
#
-
1
def head(path, initheader = nil)
-
request(Head.new(path, initheader))
-
end
-
-
# Posts +data+ (must be a String) to +path+. +header+ must be a Hash
-
# like { 'Accept' => '*/*', ... }.
-
#
-
# This method returns a Net::HTTPResponse object.
-
#
-
# If called with a block, yields each fragment of the
-
# entity body in turn as a string as it is read from
-
# the socket. Note that in this case, the returned response
-
# object will *not* contain a (meaningful) body.
-
#
-
# +dest+ argument is obsolete.
-
# It still works but you must not use it.
-
#
-
# This method never raises exception.
-
#
-
# response = http.post('/cgi-bin/search.rb', 'query=foo')
-
#
-
# # using block
-
# File.open('result.txt', 'w') {|f|
-
# http.post('/cgi-bin/search.rb', 'query=foo') do |str|
-
# f.write str
-
# end
-
# }
-
#
-
# You should set Content-Type: header field for POST.
-
# If no Content-Type: field given, this method uses
-
# "application/x-www-form-urlencoded" by default.
-
#
-
1
def post(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+
-
send_entity(path, data, initheader, dest, Post, &block)
-
end
-
-
# Sends a PATCH request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def patch(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+
-
send_entity(path, data, initheader, dest, Patch, &block)
-
end
-
-
1
def put(path, data, initheader = nil) #:nodoc:
-
request(Put.new(path, initheader), data)
-
end
-
-
# Sends a PROPPATCH request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def proppatch(path, body, initheader = nil)
-
request(Proppatch.new(path, initheader), body)
-
end
-
-
# Sends a LOCK request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def lock(path, body, initheader = nil)
-
request(Lock.new(path, initheader), body)
-
end
-
-
# Sends a UNLOCK request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def unlock(path, body, initheader = nil)
-
request(Unlock.new(path, initheader), body)
-
end
-
-
# Sends a OPTIONS request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def options(path, initheader = nil)
-
request(Options.new(path, initheader))
-
end
-
-
# Sends a PROPFIND request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def propfind(path, body = nil, initheader = {'Depth' => '0'})
-
request(Propfind.new(path, initheader), body)
-
end
-
-
# Sends a DELETE request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def delete(path, initheader = {'Depth' => 'Infinity'})
-
request(Delete.new(path, initheader))
-
end
-
-
# Sends a MOVE request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def move(path, initheader = nil)
-
request(Move.new(path, initheader))
-
end
-
-
# Sends a COPY request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def copy(path, initheader = nil)
-
request(Copy.new(path, initheader))
-
end
-
-
# Sends a MKCOL request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def mkcol(path, body = nil, initheader = nil)
-
request(Mkcol.new(path, initheader), body)
-
end
-
-
# Sends a TRACE request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def trace(path, initheader = nil)
-
request(Trace.new(path, initheader))
-
end
-
-
# Sends a GET request to the +path+.
-
# Returns the response as a Net::HTTPResponse object.
-
#
-
# When called with a block, passes an HTTPResponse object to the block.
-
# The body of the response will not have been read yet;
-
# the block can process it using HTTPResponse#read_body,
-
# if desired.
-
#
-
# Returns the response.
-
#
-
# This method never raises Net::* exceptions.
-
#
-
# response = http.request_get('/index.html')
-
# # The entity body is already read in this case.
-
# p response['content-type']
-
# puts response.body
-
#
-
# # Using a block
-
# http.request_get('/index.html') {|response|
-
# p response['content-type']
-
# response.read_body do |str| # read body now
-
# print str
-
# end
-
# }
-
#
-
1
def request_get(path, initheader = nil, &block) # :yield: +response+
-
request(Get.new(path, initheader), &block)
-
end
-
-
# Sends a HEAD request to the +path+ and returns the response
-
# as a Net::HTTPResponse object.
-
#
-
# Returns the response.
-
#
-
# This method never raises Net::* exceptions.
-
#
-
# response = http.request_head('/index.html')
-
# p response['content-type']
-
#
-
1
def request_head(path, initheader = nil, &block)
-
request(Head.new(path, initheader), &block)
-
end
-
-
# Sends a POST request to the +path+.
-
#
-
# Returns the response as a Net::HTTPResponse object.
-
#
-
# When called with a block, the block is passed an HTTPResponse
-
# object. The body of that response will not have been read yet;
-
# the block can process it using HTTPResponse#read_body, if desired.
-
#
-
# Returns the response.
-
#
-
# This method never raises Net::* exceptions.
-
#
-
# # example
-
# response = http.request_post('/cgi-bin/nice.rb', 'datadatadata...')
-
# p response.status
-
# puts response.body # body is already read in this case
-
#
-
# # using block
-
# http.request_post('/cgi-bin/nice.rb', 'datadatadata...') {|response|
-
# p response.status
-
# p response['content-type']
-
# response.read_body do |str| # read body now
-
# print str
-
# end
-
# }
-
#
-
1
def request_post(path, data, initheader = nil, &block) # :yield: +response+
-
request Post.new(path, initheader), data, &block
-
end
-
-
1
def request_put(path, data, initheader = nil, &block) #:nodoc:
-
request Put.new(path, initheader), data, &block
-
end
-
-
1
alias get2 request_get #:nodoc: obsolete
-
1
alias head2 request_head #:nodoc: obsolete
-
1
alias post2 request_post #:nodoc: obsolete
-
1
alias put2 request_put #:nodoc: obsolete
-
-
-
# Sends an HTTP request to the HTTP server.
-
# Also sends a DATA string if +data+ is given.
-
#
-
# Returns a Net::HTTPResponse object.
-
#
-
# This method never raises Net::* exceptions.
-
#
-
# response = http.send_request('GET', '/index.html')
-
# puts response.body
-
#
-
1
def send_request(name, path, data = nil, header = nil)
-
has_response_body = name != 'HEAD'
-
r = HTTPGenericRequest.new(name,(data ? true : false),has_response_body,path,header)
-
request r, data
-
end
-
-
# Sends an HTTPRequest object +req+ to the HTTP server.
-
#
-
# If +req+ is a Net::HTTP::Post or Net::HTTP::Put request containing
-
# data, the data is also sent. Providing data for a Net::HTTP::Head or
-
# Net::HTTP::Get request results in an ArgumentError.
-
#
-
# Returns an HTTPResponse object.
-
#
-
# When called with a block, passes an HTTPResponse object to the block.
-
# The body of the response will not have been read yet;
-
# the block can process it using HTTPResponse#read_body,
-
# if desired.
-
#
-
# This method never raises Net::* exceptions.
-
#
-
1
def request(req, body = nil, &block) # :yield: +response+
-
unless started?
-
start {
-
req['connection'] ||= 'close'
-
return request(req, body, &block)
-
}
-
end
-
if proxy_user()
-
req.proxy_basic_auth proxy_user(), proxy_pass() unless use_ssl?
-
end
-
req.set_body_internal body
-
res = transport_request(req, &block)
-
if sspi_auth?(res)
-
sspi_auth(req)
-
res = transport_request(req, &block)
-
end
-
res
-
end
-
-
1
private
-
-
# Executes a request which uses a representation
-
# and returns its body.
-
1
def send_entity(path, data, initheader, dest, type, &block)
-
res = nil
-
request(type.new(path, initheader), data) {|r|
-
r.read_body dest, &block
-
res = r
-
}
-
res
-
end
-
-
1
IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/ # :nodoc:
-
-
1
def transport_request(req)
-
count = 0
-
begin
-
begin_transport req
-
res = catch(:response) {
-
begin
-
req.exec @socket, @curr_http_version, edit_path(req.path)
-
rescue Errno::EPIPE
-
# Failure when writing full request, but we can probably
-
# still read the received response.
-
end
-
-
begin
-
res = HTTPResponse.read_new(@socket)
-
res.decode_content = req.decode_content
-
end while res.kind_of?(HTTPInformation)
-
-
res.uri = req.uri
-
-
res
-
}
-
res.reading_body(@socket, req.response_body_permitted?) {
-
yield res if block_given?
-
}
-
rescue Net::OpenTimeout
-
raise
-
rescue Net::ReadTimeout, IOError, EOFError,
-
Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, Errno::ETIMEDOUT,
-
# avoid a dependency on OpenSSL
-
defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError,
-
Timeout::Error => exception
-
if count < max_retries && IDEMPOTENT_METHODS_.include?(req.method)
-
count += 1
-
@socket.close if @socket
-
D "Conn close because of error #{exception}, and retry"
-
retry
-
end
-
D "Conn close because of error #{exception}"
-
@socket.close if @socket
-
raise
-
end
-
-
end_transport req, res
-
res
-
rescue => exception
-
D "Conn close because of error #{exception}"
-
@socket.close if @socket
-
raise exception
-
end
-
-
1
def begin_transport(req)
-
if @socket.closed?
-
connect
-
elsif @last_communicated
-
if @last_communicated + @keep_alive_timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC)
-
D 'Conn close because of keep_alive_timeout'
-
@socket.close
-
connect
-
elsif @socket.io.to_io.wait_readable(0) && @socket.eof?
-
D "Conn close because of EOF"
-
@socket.close
-
connect
-
end
-
end
-
-
if not req.response_body_permitted? and @close_on_empty_response
-
req['connection'] ||= 'close'
-
end
-
-
req.update_uri address, port, use_ssl?
-
req['host'] ||= addr_port()
-
end
-
-
1
def end_transport(req, res)
-
@curr_http_version = res.http_version
-
@last_communicated = nil
-
if @socket.closed?
-
D 'Conn socket closed'
-
elsif not res.body and @close_on_empty_response
-
D 'Conn close'
-
@socket.close
-
elsif keep_alive?(req, res)
-
D 'Conn keep-alive'
-
@last_communicated = Process.clock_gettime(Process::CLOCK_MONOTONIC)
-
else
-
D 'Conn close'
-
@socket.close
-
end
-
end
-
-
1
def keep_alive?(req, res)
-
return false if req.connection_close?
-
if @curr_http_version <= '1.0'
-
res.connection_keep_alive?
-
else # HTTP/1.1 or later
-
not res.connection_close?
-
end
-
end
-
-
1
def sspi_auth?(res)
-
return false unless @sspi_enabled
-
if res.kind_of?(HTTPProxyAuthenticationRequired) and
-
proxy? and res["Proxy-Authenticate"].include?("Negotiate")
-
begin
-
require 'win32/sspi'
-
true
-
rescue LoadError
-
false
-
end
-
else
-
false
-
end
-
end
-
-
1
def sspi_auth(req)
-
n = Win32::SSPI::NegotiateAuth.new
-
req["Proxy-Authorization"] = "Negotiate #{n.get_initial_token}"
-
# Some versions of ISA will close the connection if this isn't present.
-
req["Connection"] = "Keep-Alive"
-
req["Proxy-Connection"] = "Keep-Alive"
-
res = transport_request(req)
-
authphrase = res["Proxy-Authenticate"] or return res
-
req["Proxy-Authorization"] = "Negotiate #{n.complete_authentication(authphrase)}"
-
rescue => err
-
raise HTTPAuthenticationError.new('HTTP authentication failed', err)
-
end
-
-
#
-
# utils
-
#
-
-
1
private
-
-
1
def addr_port
-
addr = address
-
addr = "[#{addr}]" if addr.include?(":")
-
default_port = use_ssl? ? HTTP.https_default_port : HTTP.http_default_port
-
default_port == port ? addr : "#{addr}:#{port}"
-
end
-
-
1
def D(msg)
-
return unless @debug_output
-
@debug_output << msg
-
@debug_output << "\n"
-
end
-
end
-
-
end
-
-
1
require_relative 'http/exceptions'
-
-
1
require_relative 'http/header'
-
-
1
require_relative 'http/generic_request'
-
1
require_relative 'http/request'
-
1
require_relative 'http/requests'
-
-
1
require_relative 'http/response'
-
1
require_relative 'http/responses'
-
-
1
require_relative 'http/proxy_delta'
-
-
1
require_relative 'http/backward'
-
# frozen_string_literal: false
-
# for backward compatibility
-
-
# :enddoc:
-
-
1
class Net::HTTP
-
1
ProxyMod = ProxyDelta
-
end
-
-
1
module Net
-
1
HTTPSession = Net::HTTP
-
end
-
-
1
module Net::NetPrivate
-
1
HTTPRequest = ::Net::HTTPRequest
-
end
-
-
1
Net::HTTPInformationCode = Net::HTTPInformation
-
1
Net::HTTPSuccessCode = Net::HTTPSuccess
-
1
Net::HTTPRedirectionCode = Net::HTTPRedirection
-
1
Net::HTTPRetriableCode = Net::HTTPRedirection
-
1
Net::HTTPClientErrorCode = Net::HTTPClientError
-
1
Net::HTTPFatalErrorCode = Net::HTTPClientError
-
1
Net::HTTPServerErrorCode = Net::HTTPServerError
-
1
Net::HTTPResponceReceiver = Net::HTTPResponse
-
-
# frozen_string_literal: false
-
# Net::HTTP exception class.
-
# You cannot use Net::HTTPExceptions directly; instead, you must use
-
# its subclasses.
-
1
module Net::HTTPExceptions
-
1
def initialize(msg, res) #:nodoc:
-
super msg
-
@response = res
-
end
-
1
attr_reader :response
-
1
alias data response #:nodoc: obsolete
-
end
-
1
class Net::HTTPError < Net::ProtocolError
-
1
include Net::HTTPExceptions
-
end
-
1
class Net::HTTPRetriableError < Net::ProtoRetriableError
-
1
include Net::HTTPExceptions
-
end
-
1
class Net::HTTPServerException < Net::ProtoServerError
-
# We cannot use the name "HTTPServerError", it is the name of the response.
-
1
include Net::HTTPExceptions
-
end
-
-
# for compatibility
-
1
Net::HTTPClientException = Net::HTTPServerException
-
-
1
class Net::HTTPFatalError < Net::ProtoFatalError
-
1
include Net::HTTPExceptions
-
end
-
-
1
module Net
-
1
deprecate_constant(:HTTPServerException)
-
end
-
# frozen_string_literal: false
-
# HTTPGenericRequest is the parent of the Net::HTTPRequest class.
-
# Do not use this directly; use a subclass of Net::HTTPRequest.
-
#
-
# Mixes in the Net::HTTPHeader module to provide easier access to HTTP headers.
-
#
-
1
class Net::HTTPGenericRequest
-
-
1
include Net::HTTPHeader
-
-
1
def initialize(m, reqbody, resbody, uri_or_path, initheader = nil)
-
@method = m
-
@request_has_body = reqbody
-
@response_has_body = resbody
-
-
if URI === uri_or_path then
-
raise ArgumentError, "not an HTTP URI" unless URI::HTTP === uri_or_path
-
raise ArgumentError, "no host component for URI" unless uri_or_path.hostname
-
@uri = uri_or_path.dup
-
host = @uri.hostname.dup
-
host << ":".freeze << @uri.port.to_s if @uri.port != @uri.default_port
-
@path = uri_or_path.request_uri
-
raise ArgumentError, "no HTTP request path given" unless @path
-
else
-
@uri = nil
-
host = nil
-
raise ArgumentError, "no HTTP request path given" unless uri_or_path
-
raise ArgumentError, "HTTP request path is empty" if uri_or_path.empty?
-
@path = uri_or_path.dup
-
end
-
-
@decode_content = false
-
-
if @response_has_body and Net::HTTP::HAVE_ZLIB then
-
if !initheader ||
-
!initheader.keys.any? { |k|
-
%w[accept-encoding range].include? k.downcase
-
} then
-
@decode_content = true
-
initheader = initheader ? initheader.dup : {}
-
initheader["accept-encoding"] =
-
"gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
-
end
-
end
-
-
initialize_http_header initheader
-
self['Accept'] ||= '*/*'
-
self['User-Agent'] ||= 'Ruby'
-
self['Host'] ||= host if host
-
@body = nil
-
@body_stream = nil
-
@body_data = nil
-
end
-
-
1
attr_reader :method
-
1
attr_reader :path
-
1
attr_reader :uri
-
-
# Automatically set to false if the user sets the Accept-Encoding header.
-
# This indicates they wish to handle Content-encoding in responses
-
# themselves.
-
1
attr_reader :decode_content
-
-
1
def inspect
-
"\#<#{self.class} #{@method}>"
-
end
-
-
##
-
# Don't automatically decode response content-encoding if the user indicates
-
# they want to handle it.
-
-
1
def []=(key, val) # :nodoc:
-
@decode_content = false if key.downcase == 'accept-encoding'
-
-
super key, val
-
end
-
-
1
def request_body_permitted?
-
@request_has_body
-
end
-
-
1
def response_body_permitted?
-
@response_has_body
-
end
-
-
1
def body_exist?
-
warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?", uplevel: 1 if $VERBOSE
-
response_body_permitted?
-
end
-
-
1
attr_reader :body
-
-
1
def body=(str)
-
@body = str
-
@body_stream = nil
-
@body_data = nil
-
str
-
end
-
-
1
attr_reader :body_stream
-
-
1
def body_stream=(input)
-
@body = nil
-
@body_stream = input
-
@body_data = nil
-
input
-
end
-
-
1
def set_body_internal(str) #:nodoc: internal use only
-
raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream)
-
self.body = str if str
-
if @body.nil? && @body_stream.nil? && @body_data.nil? && request_body_permitted?
-
self.body = ''
-
end
-
end
-
-
#
-
# write
-
#
-
-
1
def exec(sock, ver, path) #:nodoc: internal use only
-
if @body
-
send_request_with_body sock, ver, path, @body
-
elsif @body_stream
-
send_request_with_body_stream sock, ver, path, @body_stream
-
elsif @body_data
-
send_request_with_body_data sock, ver, path, @body_data
-
else
-
write_header sock, ver, path
-
end
-
end
-
-
1
def update_uri(addr, port, ssl) # :nodoc: internal use only
-
# reflect the connection and @path to @uri
-
return unless @uri
-
-
if ssl
-
scheme = 'https'.freeze
-
klass = URI::HTTPS
-
else
-
scheme = 'http'.freeze
-
klass = URI::HTTP
-
end
-
-
if host = self['host']
-
host.sub!(/:.*/s, ''.freeze)
-
elsif host = @uri.host
-
else
-
host = addr
-
end
-
# convert the class of the URI
-
if @uri.is_a?(klass)
-
@uri.host = host
-
@uri.port = port
-
else
-
@uri = klass.new(
-
scheme, @uri.userinfo,
-
host, port, nil,
-
@uri.path, nil, @uri.query, nil)
-
end
-
end
-
-
1
private
-
-
1
class Chunker #:nodoc:
-
1
def initialize(sock)
-
@sock = sock
-
@prev = nil
-
end
-
-
1
def write(buf)
-
# avoid memcpy() of buf, buf can huge and eat memory bandwidth
-
rv = buf.bytesize
-
@sock.write("#{rv.to_s(16)}\r\n", buf, "\r\n")
-
rv
-
end
-
-
1
def finish
-
@sock.write("0\r\n\r\n")
-
end
-
end
-
-
1
def send_request_with_body(sock, ver, path, body)
-
self.content_length = body.bytesize
-
delete 'Transfer-Encoding'
-
supply_default_content_type
-
write_header sock, ver, path
-
wait_for_continue sock, ver if sock.continue_timeout
-
sock.write body
-
end
-
-
1
def send_request_with_body_stream(sock, ver, path, f)
-
unless content_length() or chunked?
-
raise ArgumentError,
-
"Content-Length not given and Transfer-Encoding is not `chunked'"
-
end
-
supply_default_content_type
-
write_header sock, ver, path
-
wait_for_continue sock, ver if sock.continue_timeout
-
if chunked?
-
chunker = Chunker.new(sock)
-
IO.copy_stream(f, chunker)
-
chunker.finish
-
else
-
# copy_stream can sendfile() to sock.io unless we use SSL.
-
# If sock.io is an SSLSocket, copy_stream will hit SSL_write()
-
IO.copy_stream(f, sock.io)
-
end
-
end
-
-
1
def send_request_with_body_data(sock, ver, path, params)
-
if /\Amultipart\/form-data\z/i !~ self.content_type
-
self.content_type = 'application/x-www-form-urlencoded'
-
return send_request_with_body(sock, ver, path, URI.encode_www_form(params))
-
end
-
-
opt = @form_option.dup
-
require 'securerandom' unless defined?(SecureRandom)
-
opt[:boundary] ||= SecureRandom.urlsafe_base64(40)
-
self.set_content_type(self.content_type, boundary: opt[:boundary])
-
if chunked?
-
write_header sock, ver, path
-
encode_multipart_form_data(sock, params, opt)
-
else
-
require 'tempfile'
-
file = Tempfile.new('multipart')
-
file.binmode
-
encode_multipart_form_data(file, params, opt)
-
file.rewind
-
self.content_length = file.size
-
write_header sock, ver, path
-
IO.copy_stream(file, sock)
-
file.close(true)
-
end
-
end
-
-
1
def encode_multipart_form_data(out, params, opt)
-
charset = opt[:charset]
-
boundary = opt[:boundary]
-
require 'securerandom' unless defined?(SecureRandom)
-
boundary ||= SecureRandom.urlsafe_base64(40)
-
chunked_p = chunked?
-
-
buf = ''
-
params.each do |key, value, h={}|
-
key = quote_string(key, charset)
-
filename =
-
h.key?(:filename) ? h[:filename] :
-
value.respond_to?(:to_path) ? File.basename(value.to_path) :
-
nil
-
-
buf << "--#{boundary}\r\n"
-
if filename
-
filename = quote_string(filename, charset)
-
type = h[:content_type] || 'application/octet-stream'
-
buf << "Content-Disposition: form-data; " \
-
"name=\"#{key}\"; filename=\"#{filename}\"\r\n" \
-
"Content-Type: #{type}\r\n\r\n"
-
if !out.respond_to?(:write) || !value.respond_to?(:read)
-
# if +out+ is not an IO or +value+ is not an IO
-
buf << (value.respond_to?(:read) ? value.read : value)
-
elsif value.respond_to?(:size) && chunked_p
-
# if +out+ is an IO and +value+ is a File, use IO.copy_stream
-
flush_buffer(out, buf, chunked_p)
-
out << "%x\r\n" % value.size if chunked_p
-
IO.copy_stream(value, out)
-
out << "\r\n" if chunked_p
-
else
-
# +out+ is an IO, and +value+ is not a File but an IO
-
flush_buffer(out, buf, chunked_p)
-
1 while flush_buffer(out, value.read(4096), chunked_p)
-
end
-
else
-
# non-file field:
-
# HTML5 says, "The parts of the generated multipart/form-data
-
# resource that correspond to non-file fields must not have a
-
# Content-Type header specified."
-
buf << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
-
buf << (value.respond_to?(:read) ? value.read : value)
-
end
-
buf << "\r\n"
-
end
-
buf << "--#{boundary}--\r\n"
-
flush_buffer(out, buf, chunked_p)
-
out << "0\r\n\r\n" if chunked_p
-
end
-
-
1
def quote_string(str, charset)
-
str = str.encode(charset, fallback:->(c){'&#%d;'%c.encode("UTF-8").ord}) if charset
-
str.gsub(/[\\"]/, '\\\\\&')
-
end
-
-
1
def flush_buffer(out, buf, chunked_p)
-
return unless buf
-
out << "%x\r\n"%buf.bytesize if chunked_p
-
out << buf
-
out << "\r\n" if chunked_p
-
buf.clear
-
end
-
-
1
def supply_default_content_type
-
return if content_type()
-
warn 'net/http: Content-Type did not set; using application/x-www-form-urlencoded', uplevel: 1 if $VERBOSE
-
set_content_type 'application/x-www-form-urlencoded'
-
end
-
-
##
-
# Waits up to the continue timeout for a response from the server provided
-
# we're speaking HTTP 1.1 and are expecting a 100-continue response.
-
-
1
def wait_for_continue(sock, ver)
-
if ver >= '1.1' and @header['expect'] and
-
@header['expect'].include?('100-continue')
-
if sock.io.to_io.wait_readable(sock.continue_timeout)
-
res = Net::HTTPResponse.read_new(sock)
-
unless res.kind_of?(Net::HTTPContinue)
-
res.decode_content = @decode_content
-
throw :response, res
-
end
-
end
-
end
-
end
-
-
1
def write_header(sock, ver, path)
-
reqline = "#{@method} #{path} HTTP/#{ver}"
-
if /[\r\n]/ =~ reqline
-
raise ArgumentError, "A Request-Line must not contain CR or LF"
-
end
-
buf = ""
-
buf << reqline << "\r\n"
-
each_capitalized do |k,v|
-
buf << "#{k}: #{v}\r\n"
-
end
-
buf << "\r\n"
-
sock.write buf
-
end
-
-
end
-
-
# frozen_string_literal: false
-
# The HTTPHeader module defines methods for reading and writing
-
# HTTP headers.
-
#
-
# It is used as a mixin by other classes, to provide hash-like
-
# access to HTTP header values. Unlike raw hash access, HTTPHeader
-
# provides access via case-insensitive keys. It also provides
-
# methods for accessing commonly-used HTTP header values in more
-
# convenient formats.
-
#
-
1
module Net::HTTPHeader
-
-
1
def initialize_http_header(initheader)
-
@header = {}
-
return unless initheader
-
initheader.each do |key, value|
-
warn "net/http: duplicated HTTP header: #{key}", uplevel: 3 if key?(key) and $VERBOSE
-
if value.nil?
-
warn "net/http: nil HTTP header: #{key}", uplevel: 3 if $VERBOSE
-
else
-
value = value.strip # raise error for invalid byte sequences
-
if value.count("\r\n") > 0
-
raise ArgumentError, "header #{key} has field value #{value.inspect}, this cannot include CR/LF"
-
end
-
@header[key.downcase.to_s] = [value]
-
end
-
end
-
end
-
-
1
def size #:nodoc: obsolete
-
@header.size
-
end
-
-
1
alias length size #:nodoc: obsolete
-
-
# Returns the header field corresponding to the case-insensitive key.
-
# For example, a key of "Content-Type" might return "text/html"
-
1
def [](key)
-
a = @header[key.downcase.to_s] or return nil
-
a.join(', ')
-
end
-
-
# Sets the header field corresponding to the case-insensitive key.
-
1
def []=(key, val)
-
unless val
-
@header.delete key.downcase.to_s
-
return val
-
end
-
set_field(key, val)
-
end
-
-
# [Ruby 1.8.3]
-
# Adds a value to a named header field, instead of replacing its value.
-
# Second argument +val+ must be a String.
-
# See also #[]=, #[] and #get_fields.
-
#
-
# request.add_field 'X-My-Header', 'a'
-
# p request['X-My-Header'] #=> "a"
-
# p request.get_fields('X-My-Header') #=> ["a"]
-
# request.add_field 'X-My-Header', 'b'
-
# p request['X-My-Header'] #=> "a, b"
-
# p request.get_fields('X-My-Header') #=> ["a", "b"]
-
# request.add_field 'X-My-Header', 'c'
-
# p request['X-My-Header'] #=> "a, b, c"
-
# p request.get_fields('X-My-Header') #=> ["a", "b", "c"]
-
#
-
1
def add_field(key, val)
-
stringified_downcased_key = key.downcase.to_s
-
if @header.key?(stringified_downcased_key)
-
append_field_value(@header[stringified_downcased_key], val)
-
else
-
set_field(key, val)
-
end
-
end
-
-
1
private def set_field(key, val)
-
case val
-
when Enumerable
-
ary = []
-
append_field_value(ary, val)
-
@header[key.downcase.to_s] = ary
-
else
-
val = val.to_s # for compatibility use to_s instead of to_str
-
if val.b.count("\r\n") > 0
-
raise ArgumentError, 'header field value cannot include CR/LF'
-
end
-
@header[key.downcase.to_s] = [val]
-
end
-
end
-
-
1
private def append_field_value(ary, val)
-
case val
-
when Enumerable
-
val.each{|x| append_field_value(ary, x)}
-
else
-
val = val.to_s
-
if /[\r\n]/n.match?(val.b)
-
raise ArgumentError, 'header field value cannot include CR/LF'
-
end
-
ary.push val
-
end
-
end
-
-
# [Ruby 1.8.3]
-
# Returns an array of header field strings corresponding to the
-
# case-insensitive +key+. This method allows you to get duplicated
-
# header fields without any processing. See also #[].
-
#
-
# p response.get_fields('Set-Cookie')
-
# #=> ["session=al98axx; expires=Fri, 31-Dec-1999 23:58:23",
-
# "query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"]
-
# p response['Set-Cookie']
-
# #=> "session=al98axx; expires=Fri, 31-Dec-1999 23:58:23, query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"
-
#
-
1
def get_fields(key)
-
stringified_downcased_key = key.downcase.to_s
-
return nil unless @header[stringified_downcased_key]
-
@header[stringified_downcased_key].dup
-
end
-
-
# Returns the header field corresponding to the case-insensitive key.
-
# Returns the default value +args+, or the result of the block, or
-
# raises an IndexError if there's no header field named +key+
-
# See Hash#fetch
-
1
def fetch(key, *args, &block) #:yield: +key+
-
a = @header.fetch(key.downcase.to_s, *args, &block)
-
a.kind_of?(Array) ? a.join(', ') : a
-
end
-
-
# Iterates through the header names and values, passing in the name
-
# and value to the code block supplied.
-
#
-
# Returns an enumerator if no block is given.
-
#
-
# Example:
-
#
-
# response.header.each_header {|key,value| puts "#{key} = #{value}" }
-
#
-
1
def each_header #:yield: +key+, +value+
-
block_given? or return enum_for(__method__) { @header.size }
-
@header.each do |k,va|
-
yield k, va.join(', ')
-
end
-
end
-
-
1
alias each each_header
-
-
# Iterates through the header names in the header, passing
-
# each header name to the code block.
-
#
-
# Returns an enumerator if no block is given.
-
1
def each_name(&block) #:yield: +key+
-
block_given? or return enum_for(__method__) { @header.size }
-
@header.each_key(&block)
-
end
-
-
1
alias each_key each_name
-
-
# Iterates through the header names in the header, passing
-
# capitalized header names to the code block.
-
#
-
# Note that header names are capitalized systematically;
-
# capitalization may not match that used by the remote HTTP
-
# server in its response.
-
#
-
# Returns an enumerator if no block is given.
-
1
def each_capitalized_name #:yield: +key+
-
block_given? or return enum_for(__method__) { @header.size }
-
@header.each_key do |k|
-
yield capitalize(k)
-
end
-
end
-
-
# Iterates through header values, passing each value to the
-
# code block.
-
#
-
# Returns an enumerator if no block is given.
-
1
def each_value #:yield: +value+
-
block_given? or return enum_for(__method__) { @header.size }
-
@header.each_value do |va|
-
yield va.join(', ')
-
end
-
end
-
-
# Removes a header field, specified by case-insensitive key.
-
1
def delete(key)
-
@header.delete(key.downcase.to_s)
-
end
-
-
# true if +key+ header exists.
-
1
def key?(key)
-
@header.key?(key.downcase.to_s)
-
end
-
-
# Returns a Hash consisting of header names and array of values.
-
# e.g.
-
# {"cache-control" => ["private"],
-
# "content-type" => ["text/html"],
-
# "date" => ["Wed, 22 Jun 2005 22:11:50 GMT"]}
-
1
def to_hash
-
@header.dup
-
end
-
-
# As for #each_header, except the keys are provided in capitalized form.
-
#
-
# Note that header names are capitalized systematically;
-
# capitalization may not match that used by the remote HTTP
-
# server in its response.
-
#
-
# Returns an enumerator if no block is given.
-
1
def each_capitalized
-
block_given? or return enum_for(__method__) { @header.size }
-
@header.each do |k,v|
-
yield capitalize(k), v.join(', ')
-
end
-
end
-
-
1
alias canonical_each each_capitalized
-
-
1
def capitalize(name)
-
name.to_s.split(/-/).map {|s| s.capitalize }.join('-')
-
end
-
1
private :capitalize
-
-
# Returns an Array of Range objects which represent the Range:
-
# HTTP header field, or +nil+ if there is no such header.
-
1
def range
-
return nil unless @header['range']
-
-
value = self['Range']
-
# byte-range-set = *( "," OWS ) ( byte-range-spec / suffix-byte-range-spec )
-
# *( OWS "," [ OWS ( byte-range-spec / suffix-byte-range-spec ) ] )
-
# corrected collected ABNF
-
# http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#section-5.4.1
-
# http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#appendix-C
-
# http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-19#section-3.2.5
-
unless /\Abytes=((?:,[ \t]*)*(?:\d+-\d*|-\d+)(?:[ \t]*,(?:[ \t]*\d+-\d*|-\d+)?)*)\z/ =~ value
-
raise Net::HTTPHeaderSyntaxError, "invalid syntax for byte-ranges-specifier: '#{value}'"
-
end
-
-
byte_range_set = $1
-
result = byte_range_set.split(/,/).map {|spec|
-
m = /(\d+)?\s*-\s*(\d+)?/i.match(spec) or
-
raise Net::HTTPHeaderSyntaxError, "invalid byte-range-spec: '#{spec}'"
-
d1 = m[1].to_i
-
d2 = m[2].to_i
-
if m[1] and m[2]
-
if d1 > d2
-
raise Net::HTTPHeaderSyntaxError, "last-byte-pos MUST greater than or equal to first-byte-pos but '#{spec}'"
-
end
-
d1..d2
-
elsif m[1]
-
d1..-1
-
elsif m[2]
-
-d2..-1
-
else
-
raise Net::HTTPHeaderSyntaxError, 'range is not specified'
-
end
-
}
-
# if result.empty?
-
# byte-range-set must include at least one byte-range-spec or suffix-byte-range-spec
-
# but above regexp already denies it.
-
if result.size == 1 && result[0].begin == 0 && result[0].end == -1
-
raise Net::HTTPHeaderSyntaxError, 'only one suffix-byte-range-spec with zero suffix-length'
-
end
-
result
-
end
-
-
# Sets the HTTP Range: header.
-
# Accepts either a Range object as a single argument,
-
# or a beginning index and a length from that index.
-
# Example:
-
#
-
# req.range = (0..1023)
-
# req.set_range 0, 1023
-
#
-
1
def set_range(r, e = nil)
-
unless r
-
@header.delete 'range'
-
return r
-
end
-
r = (r...r+e) if e
-
case r
-
when Numeric
-
n = r.to_i
-
rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}")
-
when Range
-
first = r.first
-
last = r.end
-
last -= 1 if r.exclude_end?
-
if last == -1
-
rangestr = (first > 0 ? "#{first}-" : "-#{-first}")
-
else
-
raise Net::HTTPHeaderSyntaxError, 'range.first is negative' if first < 0
-
raise Net::HTTPHeaderSyntaxError, 'range.last is negative' if last < 0
-
raise Net::HTTPHeaderSyntaxError, 'must be .first < .last' if first > last
-
rangestr = "#{first}-#{last}"
-
end
-
else
-
raise TypeError, 'Range/Integer is required'
-
end
-
@header['range'] = ["bytes=#{rangestr}"]
-
r
-
end
-
-
1
alias range= set_range
-
-
# Returns an Integer object which represents the HTTP Content-Length:
-
# header field, or +nil+ if that field was not provided.
-
1
def content_length
-
return nil unless key?('Content-Length')
-
len = self['Content-Length'].slice(/\d+/) or
-
raise Net::HTTPHeaderSyntaxError, 'wrong Content-Length format'
-
len.to_i
-
end
-
-
1
def content_length=(len)
-
unless len
-
@header.delete 'content-length'
-
return nil
-
end
-
@header['content-length'] = [len.to_i.to_s]
-
end
-
-
# Returns "true" if the "transfer-encoding" header is present and
-
# set to "chunked". This is an HTTP/1.1 feature, allowing
-
# the content to be sent in "chunks" without at the outset
-
# stating the entire content length.
-
1
def chunked?
-
return false unless @header['transfer-encoding']
-
field = self['Transfer-Encoding']
-
(/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false
-
end
-
-
# Returns a Range object which represents the value of the Content-Range:
-
# header field.
-
# For a partial entity body, this indicates where this fragment
-
# fits inside the full entity body, as range of byte offsets.
-
1
def content_range
-
return nil unless @header['content-range']
-
m = %r<bytes\s+(\d+)-(\d+)/(\d+|\*)>i.match(self['Content-Range']) or
-
raise Net::HTTPHeaderSyntaxError, 'wrong Content-Range format'
-
m[1].to_i .. m[2].to_i
-
end
-
-
# The length of the range represented in Content-Range: header.
-
1
def range_length
-
r = content_range() or return nil
-
r.end - r.begin + 1
-
end
-
-
# Returns a content type string such as "text/html".
-
# This method returns nil if Content-Type: header field does not exist.
-
1
def content_type
-
return nil unless main_type()
-
if sub_type()
-
then "#{main_type()}/#{sub_type()}"
-
else main_type()
-
end
-
end
-
-
# Returns a content type string such as "text".
-
# This method returns nil if Content-Type: header field does not exist.
-
1
def main_type
-
return nil unless @header['content-type']
-
self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip
-
end
-
-
# Returns a content type string such as "html".
-
# This method returns nil if Content-Type: header field does not exist
-
# or sub-type is not given (e.g. "Content-Type: text").
-
1
def sub_type
-
return nil unless @header['content-type']
-
_, sub = *self['Content-Type'].split(';').first.to_s.split('/')
-
return nil unless sub
-
sub.strip
-
end
-
-
# Any parameters specified for the content type, returned as a Hash.
-
# For example, a header of Content-Type: text/html; charset=EUC-JP
-
# would result in type_params returning {'charset' => 'EUC-JP'}
-
1
def type_params
-
result = {}
-
list = self['Content-Type'].to_s.split(';')
-
list.shift
-
list.each do |param|
-
k, v = *param.split('=', 2)
-
result[k.strip] = v.strip
-
end
-
result
-
end
-
-
# Sets the content type in an HTTP header.
-
# The +type+ should be a full HTTP content type, e.g. "text/html".
-
# The +params+ are an optional Hash of parameters to add after the
-
# content type, e.g. {'charset' => 'iso-8859-1'}
-
1
def set_content_type(type, params = {})
-
@header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')]
-
end
-
-
1
alias content_type= set_content_type
-
-
# Set header fields and a body from HTML form data.
-
# +params+ should be an Array of Arrays or
-
# a Hash containing HTML form data.
-
# Optional argument +sep+ means data record separator.
-
#
-
# Values are URL encoded as necessary and the content-type is set to
-
# application/x-www-form-urlencoded
-
#
-
# Example:
-
# http.form_data = {"q" => "ruby", "lang" => "en"}
-
# http.form_data = {"q" => ["ruby", "perl"], "lang" => "en"}
-
# http.set_form_data({"q" => "ruby", "lang" => "en"}, ';')
-
#
-
1
def set_form_data(params, sep = '&')
-
query = URI.encode_www_form(params)
-
query.gsub!(/&/, sep) if sep != '&'
-
self.body = query
-
self.content_type = 'application/x-www-form-urlencoded'
-
end
-
-
1
alias form_data= set_form_data
-
-
# Set an HTML form data set.
-
# +params+ is the form data set; it is an Array of Arrays or a Hash
-
# +enctype is the type to encode the form data set.
-
# It is application/x-www-form-urlencoded or multipart/form-data.
-
# +formopt+ is an optional hash to specify the detail.
-
#
-
# boundary:: the boundary of the multipart message
-
# charset:: the charset of the message. All names and the values of
-
# non-file fields are encoded as the charset.
-
#
-
# Each item of params is an array and contains following items:
-
# +name+:: the name of the field
-
# +value+:: the value of the field, it should be a String or a File
-
# +opt+:: an optional hash to specify additional information
-
#
-
# Each item is a file field or a normal field.
-
# If +value+ is a File object or the +opt+ have a filename key,
-
# the item is treated as a file field.
-
#
-
# If Transfer-Encoding is set as chunked, this send the request in
-
# chunked encoding. Because chunked encoding is HTTP/1.1 feature,
-
# you must confirm the server to support HTTP/1.1 before sending it.
-
#
-
# Example:
-
# http.set_form([["q", "ruby"], ["lang", "en"]])
-
#
-
# See also RFC 2388, RFC 2616, HTML 4.01, and HTML5
-
#
-
1
def set_form(params, enctype='application/x-www-form-urlencoded', formopt={})
-
@body_data = params
-
@body = nil
-
@body_stream = nil
-
@form_option = formopt
-
case enctype
-
when /\Aapplication\/x-www-form-urlencoded\z/i,
-
/\Amultipart\/form-data\z/i
-
self.content_type = enctype
-
else
-
raise ArgumentError, "invalid enctype: #{enctype}"
-
end
-
end
-
-
# Set the Authorization: header for "Basic" authorization.
-
1
def basic_auth(account, password)
-
@header['authorization'] = [basic_encode(account, password)]
-
end
-
-
# Set Proxy-Authorization: header for "Basic" authorization.
-
1
def proxy_basic_auth(account, password)
-
@header['proxy-authorization'] = [basic_encode(account, password)]
-
end
-
-
1
def basic_encode(account, password)
-
'Basic ' + ["#{account}:#{password}"].pack('m0')
-
end
-
1
private :basic_encode
-
-
1
def connection_close?
-
token = /(?:\A|,)\s*close\s*(?:\z|,)/i
-
@header['connection']&.grep(token) {return true}
-
@header['proxy-connection']&.grep(token) {return true}
-
false
-
end
-
-
1
def connection_keep_alive?
-
token = /(?:\A|,)\s*keep-alive\s*(?:\z|,)/i
-
@header['connection']&.grep(token) {return true}
-
@header['proxy-connection']&.grep(token) {return true}
-
false
-
end
-
-
end
-
# frozen_string_literal: false
-
1
module Net::HTTP::ProxyDelta #:nodoc: internal use only
-
1
private
-
-
1
def conn_address
-
proxy_address()
-
end
-
-
1
def conn_port
-
proxy_port()
-
end
-
-
1
def edit_path(path)
-
use_ssl? ? path : "http://#{addr_port()}#{path}"
-
end
-
end
-
-
# frozen_string_literal: false
-
# HTTP request class.
-
# This class wraps together the request header and the request path.
-
# You cannot use this class directly. Instead, you should use one of its
-
# subclasses: Net::HTTP::Get, Net::HTTP::Post, Net::HTTP::Head.
-
#
-
1
class Net::HTTPRequest < Net::HTTPGenericRequest
-
# Creates an HTTP request object for +path+.
-
#
-
# +initheader+ are the default headers to use. Net::HTTP adds
-
# Accept-Encoding to enable compression of the response body unless
-
# Accept-Encoding or Range are supplied in +initheader+.
-
-
1
def initialize(path, initheader = nil)
-
super self.class::METHOD,
-
self.class::REQUEST_HAS_BODY,
-
self.class::RESPONSE_HAS_BODY,
-
path, initheader
-
end
-
end
-
-
# frozen_string_literal: false
-
# HTTP response class.
-
#
-
# This class wraps together the response header and the response body (the
-
# entity requested).
-
#
-
# It mixes in the HTTPHeader module, which provides access to response
-
# header values both via hash-like methods and via individual readers.
-
#
-
# Note that each possible HTTP response code defines its own
-
# HTTPResponse subclass. All classes are defined under the Net module.
-
# Indentation indicates inheritance. For a list of the classes see Net::HTTP.
-
#
-
# Correspondence <code>HTTP code => class</code> is stored in CODE_TO_OBJ
-
# constant:
-
#
-
# Net::HTTPResponse::CODE_TO_OBJ['404'] #=> Net::HTTPNotFound
-
#
-
1
class Net::HTTPResponse
-
1
class << self
-
# true if the response has a body.
-
1
def body_permitted?
-
self::HAS_BODY
-
end
-
-
1
def exception_type # :nodoc: internal use only
-
self::EXCEPTION_TYPE
-
end
-
-
1
def read_new(sock) #:nodoc: internal use only
-
httpv, code, msg = read_status_line(sock)
-
res = response_class(code).new(httpv, code, msg)
-
each_response_header(sock) do |k,v|
-
res.add_field k, v
-
end
-
res
-
end
-
-
1
private
-
-
1
def read_status_line(sock)
-
str = sock.readline
-
m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\z/in.match(str) or
-
raise Net::HTTPBadResponse, "wrong status line: #{str.dump}"
-
m.captures
-
end
-
-
1
def response_class(code)
-
CODE_TO_OBJ[code] or
-
CODE_CLASS_TO_OBJ[code[0,1]] or
-
Net::HTTPUnknownResponse
-
end
-
-
1
def each_response_header(sock)
-
key = value = nil
-
while true
-
line = sock.readuntil("\n", true).sub(/\s+\z/, '')
-
break if line.empty?
-
if line[0] == ?\s or line[0] == ?\t and value
-
value << ' ' unless value.empty?
-
value << line.strip
-
else
-
yield key, value if key
-
key, value = line.strip.split(/\s*:\s*/, 2)
-
raise Net::HTTPBadResponse, 'wrong header line format' if value.nil?
-
end
-
end
-
yield key, value if key
-
end
-
end
-
-
# next is to fix bug in RDoc, where the private inside class << self
-
# spills out.
-
1
public
-
-
1
include Net::HTTPHeader
-
-
1
def initialize(httpv, code, msg) #:nodoc: internal use only
-
@http_version = httpv
-
@code = code
-
@message = msg
-
initialize_http_header nil
-
@body = nil
-
@read = false
-
@uri = nil
-
@decode_content = false
-
end
-
-
# The HTTP version supported by the server.
-
1
attr_reader :http_version
-
-
# The HTTP result code string. For example, '302'. You can also
-
# determine the response type by examining which response subclass
-
# the response object is an instance of.
-
1
attr_reader :code
-
-
# The HTTP result message sent by the server. For example, 'Not Found'.
-
1
attr_reader :message
-
1
alias msg message # :nodoc: obsolete
-
-
# The URI used to fetch this response. The response URI is only available
-
# if a URI was used to create the request.
-
1
attr_reader :uri
-
-
# Set to true automatically when the request did not contain an
-
# Accept-Encoding header from the user.
-
1
attr_accessor :decode_content
-
-
1
def inspect
-
"#<#{self.class} #{@code} #{@message} readbody=#{@read}>"
-
end
-
-
#
-
# response <-> exception relationship
-
#
-
-
1
def code_type #:nodoc:
-
self.class
-
end
-
-
1
def error! #:nodoc:
-
message = @code
-
message += ' ' + @message.dump if @message
-
raise error_type().new(message, self)
-
end
-
-
1
def error_type #:nodoc:
-
self.class::EXCEPTION_TYPE
-
end
-
-
# Raises an HTTP error if the response is not 2xx (success).
-
1
def value
-
error! unless self.kind_of?(Net::HTTPSuccess)
-
end
-
-
1
def uri= uri # :nodoc:
-
@uri = uri.dup if uri
-
end
-
-
#
-
# header (for backward compatibility only; DO NOT USE)
-
#
-
-
1
def response #:nodoc:
-
warn "Net::HTTPResponse#response is obsolete", uplevel: 1 if $VERBOSE
-
self
-
end
-
-
1
def header #:nodoc:
-
warn "Net::HTTPResponse#header is obsolete", uplevel: 1 if $VERBOSE
-
self
-
end
-
-
1
def read_header #:nodoc:
-
warn "Net::HTTPResponse#read_header is obsolete", uplevel: 1 if $VERBOSE
-
self
-
end
-
-
#
-
# body
-
#
-
-
1
def reading_body(sock, reqmethodallowbody) #:nodoc: internal use only
-
@socket = sock
-
@body_exist = reqmethodallowbody && self.class.body_permitted?
-
begin
-
yield
-
self.body # ensure to read body
-
ensure
-
@socket = nil
-
end
-
end
-
-
# Gets the entity body returned by the remote HTTP server.
-
#
-
# If a block is given, the body is passed to the block, and
-
# the body is provided in fragments, as it is read in from the socket.
-
#
-
# If +dest+ argument is given, response is read into that variable,
-
# with <code>dest#<<</code> method (it could be String or IO, or any
-
# other object responding to <code><<</code>).
-
#
-
# Calling this method a second or subsequent time for the same
-
# HTTPResponse object will return the value already read.
-
#
-
# http.request_get('/index.html') {|res|
-
# puts res.read_body
-
# }
-
#
-
# http.request_get('/index.html') {|res|
-
# p res.read_body.object_id # 538149362
-
# p res.read_body.object_id # 538149362
-
# }
-
#
-
# # using iterator
-
# http.request_get('/index.html') {|res|
-
# res.read_body do |segment|
-
# print segment
-
# end
-
# }
-
#
-
1
def read_body(dest = nil, &block)
-
if @read
-
raise IOError, "#{self.class}\#read_body called twice" if dest or block
-
return @body
-
end
-
to = procdest(dest, block)
-
stream_check
-
if @body_exist
-
read_body_0 to
-
@body = to
-
else
-
@body = nil
-
end
-
@read = true
-
-
@body
-
end
-
-
# Returns the full entity body.
-
#
-
# Calling this method a second or subsequent time will return the
-
# string already read.
-
#
-
# http.request_get('/index.html') {|res|
-
# puts res.body
-
# }
-
#
-
# http.request_get('/index.html') {|res|
-
# p res.body.object_id # 538149362
-
# p res.body.object_id # 538149362
-
# }
-
#
-
1
def body
-
read_body()
-
end
-
-
# Because it may be necessary to modify the body, Eg, decompression
-
# this method facilitates that.
-
1
def body=(value)
-
@body = value
-
end
-
-
1
alias entity body #:nodoc: obsolete
-
-
1
private
-
-
##
-
# Checks for a supported Content-Encoding header and yields an Inflate
-
# wrapper for this response's socket when zlib is present. If the
-
# Content-Encoding is not supported or zlib is missing, the plain socket is
-
# yielded.
-
#
-
# If a Content-Range header is present, a plain socket is yielded as the
-
# bytes in the range may not be a complete deflate block.
-
-
1
def inflater # :nodoc:
-
return yield @socket unless Net::HTTP::HAVE_ZLIB
-
return yield @socket unless @decode_content
-
return yield @socket if self['content-range']
-
-
v = self['content-encoding']
-
case v&.downcase
-
when 'deflate', 'gzip', 'x-gzip' then
-
self.delete 'content-encoding'
-
-
inflate_body_io = Inflater.new(@socket)
-
-
begin
-
yield inflate_body_io
-
ensure
-
orig_err = $!
-
begin
-
inflate_body_io.finish
-
rescue => err
-
raise orig_err || err
-
end
-
end
-
when 'none', 'identity' then
-
self.delete 'content-encoding'
-
-
yield @socket
-
else
-
yield @socket
-
end
-
end
-
-
1
def read_body_0(dest)
-
inflater do |inflate_body_io|
-
if chunked?
-
read_chunked dest, inflate_body_io
-
return
-
end
-
-
@socket = inflate_body_io
-
-
clen = content_length()
-
if clen
-
@socket.read clen, dest, true # ignore EOF
-
return
-
end
-
clen = range_length()
-
if clen
-
@socket.read clen, dest
-
return
-
end
-
@socket.read_all dest
-
end
-
end
-
-
##
-
# read_chunked reads from +@socket+ for chunk-size, chunk-extension, CRLF,
-
# etc. and +chunk_data_io+ for chunk-data which may be deflate or gzip
-
# encoded.
-
#
-
# See RFC 2616 section 3.6.1 for definitions
-
-
1
def read_chunked(dest, chunk_data_io) # :nodoc:
-
total = 0
-
while true
-
line = @socket.readline
-
hexlen = line.slice(/[0-9a-fA-F]+/) or
-
raise Net::HTTPBadResponse, "wrong chunk size line: #{line}"
-
len = hexlen.hex
-
break if len == 0
-
begin
-
chunk_data_io.read len, dest
-
ensure
-
total += len
-
@socket.read 2 # \r\n
-
end
-
end
-
until @socket.readline.empty?
-
# none
-
end
-
end
-
-
1
def stream_check
-
raise IOError, 'attempt to read body out of block' if @socket.closed?
-
end
-
-
1
def procdest(dest, block)
-
raise ArgumentError, 'both arg and block given for HTTP method' if
-
dest and block
-
if block
-
Net::ReadAdapter.new(block)
-
else
-
dest || ''
-
end
-
end
-
-
##
-
# Inflater is a wrapper around Net::BufferedIO that transparently inflates
-
# zlib and gzip streams.
-
-
1
class Inflater # :nodoc:
-
-
##
-
# Creates a new Inflater wrapping +socket+
-
-
1
def initialize socket
-
@socket = socket
-
# zlib with automatic gzip detection
-
@inflate = Zlib::Inflate.new(32 + Zlib::MAX_WBITS)
-
end
-
-
##
-
# Finishes the inflate stream.
-
-
1
def finish
-
return if @inflate.total_in == 0
-
@inflate.finish
-
end
-
-
##
-
# Returns a Net::ReadAdapter that inflates each read chunk into +dest+.
-
#
-
# This allows a large response body to be inflated without storing the
-
# entire body in memory.
-
-
1
def inflate_adapter(dest)
-
if dest.respond_to?(:set_encoding)
-
dest.set_encoding(Encoding::ASCII_8BIT)
-
elsif dest.respond_to?(:force_encoding)
-
dest.force_encoding(Encoding::ASCII_8BIT)
-
end
-
block = proc do |compressed_chunk|
-
@inflate.inflate(compressed_chunk) do |chunk|
-
compressed_chunk.clear
-
dest << chunk
-
end
-
end
-
-
Net::ReadAdapter.new(block)
-
end
-
-
##
-
# Reads +clen+ bytes from the socket, inflates them, then writes them to
-
# +dest+. +ignore_eof+ is passed down to Net::BufferedIO#read
-
#
-
# Unlike Net::BufferedIO#read, this method returns more than +clen+ bytes.
-
# At this time there is no way for a user of Net::HTTPResponse to read a
-
# specific number of bytes from the HTTP response body, so this internal
-
# API does not return the same number of bytes as were requested.
-
#
-
# See https://bugs.ruby-lang.org/issues/6492 for further discussion.
-
-
1
def read clen, dest, ignore_eof = false
-
temp_dest = inflate_adapter(dest)
-
-
@socket.read clen, temp_dest, ignore_eof
-
end
-
-
##
-
# Reads the rest of the socket, inflates it, then writes it to +dest+.
-
-
1
def read_all dest
-
temp_dest = inflate_adapter(dest)
-
-
@socket.read_all temp_dest
-
end
-
-
end
-
-
end
-
-
# frozen_string_literal: true
-
# :stopdoc:
-
# https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
-
1
class Net::HTTPUnknownResponse < Net::HTTPResponse
-
1
HAS_BODY = true
-
1
EXCEPTION_TYPE = Net::HTTPError
-
end
-
1
class Net::HTTPInformation < Net::HTTPResponse # 1xx
-
1
HAS_BODY = false
-
1
EXCEPTION_TYPE = Net::HTTPError
-
end
-
1
class Net::HTTPSuccess < Net::HTTPResponse # 2xx
-
1
HAS_BODY = true
-
1
EXCEPTION_TYPE = Net::HTTPError
-
end
-
1
class Net::HTTPRedirection < Net::HTTPResponse # 3xx
-
1
HAS_BODY = true
-
1
EXCEPTION_TYPE = Net::HTTPRetriableError
-
end
-
1
class Net::HTTPClientError < Net::HTTPResponse # 4xx
-
1
HAS_BODY = true
-
1
EXCEPTION_TYPE = Net::HTTPClientException # for backward compatibility
-
end
-
1
class Net::HTTPServerError < Net::HTTPResponse # 5xx
-
1
HAS_BODY = true
-
1
EXCEPTION_TYPE = Net::HTTPFatalError # for backward compatibility
-
end
-
-
1
class Net::HTTPContinue < Net::HTTPInformation # 100
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPSwitchProtocol < Net::HTTPInformation # 101
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPProcessing < Net::HTTPInformation # 102
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPEarlyHints < Net::HTTPInformation # 103 - RFC 8297
-
1
HAS_BODY = false
-
end
-
-
1
class Net::HTTPOK < Net::HTTPSuccess # 200
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPCreated < Net::HTTPSuccess # 201
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPAccepted < Net::HTTPSuccess # 202
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNonAuthoritativeInformation < Net::HTTPSuccess # 203
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNoContent < Net::HTTPSuccess # 204
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPResetContent < Net::HTTPSuccess # 205
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPPartialContent < Net::HTTPSuccess # 206
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPMultiStatus < Net::HTTPSuccess # 207 - RFC 4918
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPAlreadyReported < Net::HTTPSuccess # 208 - RFC 5842
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPIMUsed < Net::HTTPSuccess # 226 - RFC 3229
-
1
HAS_BODY = true
-
end
-
-
1
class Net::HTTPMultipleChoices < Net::HTTPRedirection # 300
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPMultipleChoice = Net::HTTPMultipleChoices
-
1
class Net::HTTPMovedPermanently < Net::HTTPRedirection # 301
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPFound < Net::HTTPRedirection # 302
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPMovedTemporarily = Net::HTTPFound
-
1
class Net::HTTPSeeOther < Net::HTTPRedirection # 303
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNotModified < Net::HTTPRedirection # 304
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPUseProxy < Net::HTTPRedirection # 305
-
1
HAS_BODY = false
-
end
-
# 306 Switch Proxy - no longer unused
-
1
class Net::HTTPTemporaryRedirect < Net::HTTPRedirection # 307
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPPermanentRedirect < Net::HTTPRedirection # 308
-
1
HAS_BODY = true
-
end
-
-
1
class Net::HTTPBadRequest < Net::HTTPClientError # 400
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPUnauthorized < Net::HTTPClientError # 401
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPPaymentRequired < Net::HTTPClientError # 402
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPForbidden < Net::HTTPClientError # 403
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNotFound < Net::HTTPClientError # 404
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPMethodNotAllowed < Net::HTTPClientError # 405
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNotAcceptable < Net::HTTPClientError # 406
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPProxyAuthenticationRequired < Net::HTTPClientError # 407
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPRequestTimeout < Net::HTTPClientError # 408
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPRequestTimeOut = Net::HTTPRequestTimeout
-
1
class Net::HTTPConflict < Net::HTTPClientError # 409
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPGone < Net::HTTPClientError # 410
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPLengthRequired < Net::HTTPClientError # 411
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPPreconditionFailed < Net::HTTPClientError # 412
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPPayloadTooLarge < Net::HTTPClientError # 413
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPRequestEntityTooLarge = Net::HTTPPayloadTooLarge
-
1
class Net::HTTPURITooLong < Net::HTTPClientError # 414
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPRequestURITooLong = Net::HTTPURITooLong
-
1
Net::HTTPRequestURITooLarge = Net::HTTPRequestURITooLong
-
1
class Net::HTTPUnsupportedMediaType < Net::HTTPClientError # 415
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPRangeNotSatisfiable < Net::HTTPClientError # 416
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPRequestedRangeNotSatisfiable = Net::HTTPRangeNotSatisfiable
-
1
class Net::HTTPExpectationFailed < Net::HTTPClientError # 417
-
1
HAS_BODY = true
-
end
-
# 418 I'm a teapot - RFC 2324; a joke RFC
-
# 420 Enhance Your Calm - Twitter
-
1
class Net::HTTPMisdirectedRequest < Net::HTTPClientError # 421 - RFC 7540
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPUnprocessableEntity < Net::HTTPClientError # 422 - RFC 4918
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPLocked < Net::HTTPClientError # 423 - RFC 4918
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPFailedDependency < Net::HTTPClientError # 424 - RFC 4918
-
1
HAS_BODY = true
-
end
-
# 425 Unordered Collection - existed only in draft
-
1
class Net::HTTPUpgradeRequired < Net::HTTPClientError # 426 - RFC 2817
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPPreconditionRequired < Net::HTTPClientError # 428 - RFC 6585
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPTooManyRequests < Net::HTTPClientError # 429 - RFC 6585
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPRequestHeaderFieldsTooLarge < Net::HTTPClientError # 431 - RFC 6585
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPUnavailableForLegalReasons < Net::HTTPClientError # 451 - RFC 7725
-
1
HAS_BODY = true
-
end
-
# 444 No Response - Nginx
-
# 449 Retry With - Microsoft
-
# 450 Blocked by Windows Parental Controls - Microsoft
-
# 499 Client Closed Request - Nginx
-
-
1
class Net::HTTPInternalServerError < Net::HTTPServerError # 500
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNotImplemented < Net::HTTPServerError # 501
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPBadGateway < Net::HTTPServerError # 502
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPServiceUnavailable < Net::HTTPServerError # 503
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPGatewayTimeout < Net::HTTPServerError # 504
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPGatewayTimeOut = Net::HTTPGatewayTimeout
-
1
class Net::HTTPVersionNotSupported < Net::HTTPServerError # 505
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPVariantAlsoNegotiates < Net::HTTPServerError # 506
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPInsufficientStorage < Net::HTTPServerError # 507 - RFC 4918
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPLoopDetected < Net::HTTPServerError # 508 - RFC 5842
-
1
HAS_BODY = true
-
end
-
# 509 Bandwidth Limit Exceeded - Apache bw/limited extension
-
1
class Net::HTTPNotExtended < Net::HTTPServerError # 510 - RFC 2774
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNetworkAuthenticationRequired < Net::HTTPServerError # 511 - RFC 6585
-
1
HAS_BODY = true
-
end
-
-
1
class Net::HTTPResponse
-
CODE_CLASS_TO_OBJ = {
-
1
'1' => Net::HTTPInformation,
-
'2' => Net::HTTPSuccess,
-
'3' => Net::HTTPRedirection,
-
'4' => Net::HTTPClientError,
-
'5' => Net::HTTPServerError
-
}
-
CODE_TO_OBJ = {
-
1
'100' => Net::HTTPContinue,
-
'101' => Net::HTTPSwitchProtocol,
-
'102' => Net::HTTPProcessing,
-
'103' => Net::HTTPEarlyHints,
-
-
'200' => Net::HTTPOK,
-
'201' => Net::HTTPCreated,
-
'202' => Net::HTTPAccepted,
-
'203' => Net::HTTPNonAuthoritativeInformation,
-
'204' => Net::HTTPNoContent,
-
'205' => Net::HTTPResetContent,
-
'206' => Net::HTTPPartialContent,
-
'207' => Net::HTTPMultiStatus,
-
'208' => Net::HTTPAlreadyReported,
-
'226' => Net::HTTPIMUsed,
-
-
'300' => Net::HTTPMultipleChoices,
-
'301' => Net::HTTPMovedPermanently,
-
'302' => Net::HTTPFound,
-
'303' => Net::HTTPSeeOther,
-
'304' => Net::HTTPNotModified,
-
'305' => Net::HTTPUseProxy,
-
'307' => Net::HTTPTemporaryRedirect,
-
'308' => Net::HTTPPermanentRedirect,
-
-
'400' => Net::HTTPBadRequest,
-
'401' => Net::HTTPUnauthorized,
-
'402' => Net::HTTPPaymentRequired,
-
'403' => Net::HTTPForbidden,
-
'404' => Net::HTTPNotFound,
-
'405' => Net::HTTPMethodNotAllowed,
-
'406' => Net::HTTPNotAcceptable,
-
'407' => Net::HTTPProxyAuthenticationRequired,
-
'408' => Net::HTTPRequestTimeout,
-
'409' => Net::HTTPConflict,
-
'410' => Net::HTTPGone,
-
'411' => Net::HTTPLengthRequired,
-
'412' => Net::HTTPPreconditionFailed,
-
'413' => Net::HTTPPayloadTooLarge,
-
'414' => Net::HTTPURITooLong,
-
'415' => Net::HTTPUnsupportedMediaType,
-
'416' => Net::HTTPRangeNotSatisfiable,
-
'417' => Net::HTTPExpectationFailed,
-
'421' => Net::HTTPMisdirectedRequest,
-
'422' => Net::HTTPUnprocessableEntity,
-
'423' => Net::HTTPLocked,
-
'424' => Net::HTTPFailedDependency,
-
'426' => Net::HTTPUpgradeRequired,
-
'428' => Net::HTTPPreconditionRequired,
-
'429' => Net::HTTPTooManyRequests,
-
'431' => Net::HTTPRequestHeaderFieldsTooLarge,
-
'451' => Net::HTTPUnavailableForLegalReasons,
-
-
'500' => Net::HTTPInternalServerError,
-
'501' => Net::HTTPNotImplemented,
-
'502' => Net::HTTPBadGateway,
-
'503' => Net::HTTPServiceUnavailable,
-
'504' => Net::HTTPGatewayTimeout,
-
'505' => Net::HTTPVersionNotSupported,
-
'506' => Net::HTTPVariantAlsoNegotiates,
-
'507' => Net::HTTPInsufficientStorage,
-
'508' => Net::HTTPLoopDetected,
-
'510' => Net::HTTPNotExtended,
-
'511' => Net::HTTPNetworkAuthenticationRequired,
-
}
-
end
-
-
# :startdoc:
-
# frozen_string_literal: true
-
#
-
# = net/protocol.rb
-
#
-
#--
-
# Copyright (c) 1999-2004 Yukihiro Matsumoto
-
# Copyright (c) 1999-2004 Minero Aoki
-
#
-
# written and maintained by Minero Aoki <aamine@loveruby.net>
-
#
-
# This program is free software. You can re-distribute and/or
-
# modify this program under the same terms as Ruby itself,
-
# Ruby Distribute License or GNU General Public License.
-
#
-
# $Id$
-
#++
-
#
-
# WARNING: This file is going to remove.
-
# Do not rely on the implementation written in this file.
-
#
-
-
1
require 'socket'
-
1
require 'timeout'
-
1
require 'io/wait'
-
-
1
module Net # :nodoc:
-
-
1
class Protocol #:nodoc: internal use only
-
1
private
-
1
def Protocol.protocol_param(name, val)
-
module_eval(<<-End, __FILE__, __LINE__ + 1)
-
def #{name}
-
#{val}
-
end
-
End
-
end
-
-
1
def ssl_socket_connect(s, timeout)
-
if timeout
-
while true
-
raise Net::OpenTimeout if timeout <= 0
-
start = Process.clock_gettime Process::CLOCK_MONOTONIC
-
# to_io is required because SSLSocket doesn't have wait_readable yet
-
case s.connect_nonblock(exception: false)
-
when :wait_readable; s.to_io.wait_readable(timeout)
-
when :wait_writable; s.to_io.wait_writable(timeout)
-
else; break
-
end
-
timeout -= Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
-
end
-
else
-
s.connect
-
end
-
end
-
end
-
-
-
1
class ProtocolError < StandardError; end
-
1
class ProtoSyntaxError < ProtocolError; end
-
1
class ProtoFatalError < ProtocolError; end
-
1
class ProtoUnknownError < ProtocolError; end
-
1
class ProtoServerError < ProtocolError; end
-
1
class ProtoAuthError < ProtocolError; end
-
1
class ProtoCommandError < ProtocolError; end
-
1
class ProtoRetriableError < ProtocolError; end
-
1
ProtocRetryError = ProtoRetriableError
-
-
##
-
# OpenTimeout, a subclass of Timeout::Error, is raised if a connection cannot
-
# be created within the open_timeout.
-
-
1
class OpenTimeout < Timeout::Error; end
-
-
##
-
# ReadTimeout, a subclass of Timeout::Error, is raised if a chunk of the
-
# response cannot be read within the read_timeout.
-
-
1
class ReadTimeout < Timeout::Error
-
1
def initialize(io = nil)
-
@io = io
-
end
-
1
attr_reader :io
-
-
1
def message
-
msg = super
-
if @io
-
msg = "#{msg} with #{@io.inspect}"
-
end
-
msg
-
end
-
end
-
-
##
-
# WriteTimeout, a subclass of Timeout::Error, is raised if a chunk of the
-
# response cannot be written within the write_timeout. Not raised on Windows.
-
-
1
class WriteTimeout < Timeout::Error
-
1
def initialize(io = nil)
-
@io = io
-
end
-
1
attr_reader :io
-
-
1
def message
-
msg = super
-
if @io
-
msg = "#{msg} with #{@io.inspect}"
-
end
-
msg
-
end
-
end
-
-
-
1
class BufferedIO #:nodoc: internal use only
-
1
def initialize(io, read_timeout: 60, write_timeout: 60, continue_timeout: nil, debug_output: nil)
-
@io = io
-
@read_timeout = read_timeout
-
@write_timeout = write_timeout
-
@continue_timeout = continue_timeout
-
@debug_output = debug_output
-
@rbuf = ''.b
-
end
-
-
1
attr_reader :io
-
1
attr_accessor :read_timeout
-
1
attr_accessor :write_timeout
-
1
attr_accessor :continue_timeout
-
1
attr_accessor :debug_output
-
-
1
def inspect
-
"#<#{self.class} io=#{@io}>"
-
end
-
-
1
def eof?
-
@io.eof?
-
end
-
-
1
def closed?
-
@io.closed?
-
end
-
-
1
def close
-
@io.close
-
end
-
-
#
-
# Read
-
#
-
-
1
public
-
-
1
def read(len, dest = ''.b, ignore_eof = false)
-
LOG "reading #{len} bytes..."
-
read_bytes = 0
-
begin
-
while read_bytes + @rbuf.size < len
-
s = rbuf_consume(@rbuf.size)
-
read_bytes += s.size
-
dest << s
-
rbuf_fill
-
end
-
s = rbuf_consume(len - read_bytes)
-
read_bytes += s.size
-
dest << s
-
rescue EOFError
-
raise unless ignore_eof
-
end
-
LOG "read #{read_bytes} bytes"
-
dest
-
end
-
-
1
def read_all(dest = ''.b)
-
LOG 'reading all...'
-
read_bytes = 0
-
begin
-
while true
-
s = rbuf_consume(@rbuf.size)
-
read_bytes += s.size
-
dest << s
-
rbuf_fill
-
end
-
rescue EOFError
-
;
-
end
-
LOG "read #{read_bytes} bytes"
-
dest
-
end
-
-
1
def readuntil(terminator, ignore_eof = false)
-
begin
-
until idx = @rbuf.index(terminator)
-
rbuf_fill
-
end
-
return rbuf_consume(idx + terminator.size)
-
rescue EOFError
-
raise unless ignore_eof
-
return rbuf_consume(@rbuf.size)
-
end
-
end
-
-
1
def readline
-
readuntil("\n").chop
-
end
-
-
1
private
-
-
1
BUFSIZE = 1024 * 16
-
-
1
def rbuf_fill
-
tmp = @rbuf.empty? ? @rbuf : nil
-
case rv = @io.read_nonblock(BUFSIZE, tmp, exception: false)
-
when String
-
return if rv.equal?(tmp)
-
@rbuf << rv
-
rv.clear
-
return
-
when :wait_readable
-
(io = @io.to_io).wait_readable(@read_timeout) or raise Net::ReadTimeout.new(io)
-
# continue looping
-
when :wait_writable
-
# OpenSSL::Buffering#read_nonblock may fail with IO::WaitWritable.
-
# http://www.openssl.org/support/faq.html#PROG10
-
(io = @io.to_io).wait_writable(@read_timeout) or raise Net::ReadTimeout.new(io)
-
# continue looping
-
when nil
-
raise EOFError, 'end of file reached'
-
end while true
-
end
-
-
1
def rbuf_consume(len)
-
if len == @rbuf.size
-
s = @rbuf
-
@rbuf = ''.b
-
else
-
s = @rbuf.slice!(0, len)
-
end
-
@debug_output << %Q[-> #{s.dump}\n] if @debug_output
-
s
-
end
-
-
#
-
# Write
-
#
-
-
1
public
-
-
1
def write(*strs)
-
writing {
-
write0(*strs)
-
}
-
end
-
-
1
alias << write
-
-
1
def writeline(str)
-
writing {
-
write0 str + "\r\n"
-
}
-
end
-
-
1
private
-
-
1
def writing
-
@written_bytes = 0
-
@debug_output << '<- ' if @debug_output
-
yield
-
@debug_output << "\n" if @debug_output
-
bytes = @written_bytes
-
@written_bytes = nil
-
bytes
-
end
-
-
1
def write0(*strs)
-
@debug_output << strs.map(&:dump).join if @debug_output
-
orig_written_bytes = @written_bytes
-
strs.each_with_index do |str, i|
-
need_retry = true
-
case len = @io.write_nonblock(str, exception: false)
-
when Integer
-
@written_bytes += len
-
len -= str.bytesize
-
if len == 0
-
if strs.size == i+1
-
return @written_bytes - orig_written_bytes
-
else
-
need_retry = false
-
# next string
-
end
-
elsif len < 0
-
str = str.byteslice(len, -len)
-
else # len > 0
-
need_retry = false
-
# next string
-
end
-
# continue looping
-
when :wait_writable
-
(io = @io.to_io).wait_writable(@write_timeout) or raise Net::WriteTimeout.new(io)
-
# continue looping
-
end while need_retry
-
end
-
end
-
-
#
-
# Logging
-
#
-
-
1
private
-
-
1
def LOG_off
-
@save_debug_out = @debug_output
-
@debug_output = nil
-
end
-
-
1
def LOG_on
-
@debug_output = @save_debug_out
-
end
-
-
1
def LOG(msg)
-
return unless @debug_output
-
@debug_output << msg + "\n"
-
end
-
end
-
-
-
1
class InternetMessageIO < BufferedIO #:nodoc: internal use only
-
1
def initialize(*, **)
-
super
-
@wbuf = nil
-
end
-
-
#
-
# Read
-
#
-
-
1
def each_message_chunk
-
LOG 'reading message...'
-
LOG_off()
-
read_bytes = 0
-
while (line = readuntil("\r\n")) != ".\r\n"
-
read_bytes += line.size
-
yield line.delete_prefix('.')
-
end
-
LOG_on()
-
LOG "read message (#{read_bytes} bytes)"
-
end
-
-
# *library private* (cannot handle 'break')
-
1
def each_list_item
-
while (str = readuntil("\r\n")) != ".\r\n"
-
yield str.chop
-
end
-
end
-
-
1
def write_message_0(src)
-
prev = @written_bytes
-
each_crlf_line(src) do |line|
-
write0 dot_stuff(line)
-
end
-
@written_bytes - prev
-
end
-
-
#
-
# Write
-
#
-
-
1
def write_message(src)
-
LOG "writing message from #{src.class}"
-
LOG_off()
-
len = writing {
-
using_each_crlf_line {
-
write_message_0 src
-
}
-
}
-
LOG_on()
-
LOG "wrote #{len} bytes"
-
len
-
end
-
-
1
def write_message_by_block(&block)
-
LOG 'writing message from block'
-
LOG_off()
-
len = writing {
-
using_each_crlf_line {
-
begin
-
block.call(WriteAdapter.new(self, :write_message_0))
-
rescue LocalJumpError
-
# allow `break' from writer block
-
end
-
}
-
}
-
LOG_on()
-
LOG "wrote #{len} bytes"
-
len
-
end
-
-
1
private
-
-
1
def dot_stuff(s)
-
s.sub(/\A\./, '..')
-
end
-
-
1
def using_each_crlf_line
-
@wbuf = ''.b
-
yield
-
if not @wbuf.empty? # unterminated last line
-
write0 dot_stuff(@wbuf.chomp) + "\r\n"
-
elsif @written_bytes == 0 # empty src
-
write0 "\r\n"
-
end
-
write0 ".\r\n"
-
@wbuf = nil
-
end
-
-
1
def each_crlf_line(src)
-
buffer_filling(@wbuf, src) do
-
while line = @wbuf.slice!(/\A[^\r\n]*(?:\n|\r(?:\n|(?!\z)))/)
-
yield line.chomp("\n") + "\r\n"
-
end
-
end
-
end
-
-
1
def buffer_filling(buf, src)
-
case src
-
when String # for speeding up.
-
0.step(src.size - 1, 1024) do |i|
-
buf << src[i, 1024]
-
yield
-
end
-
when File # for speeding up.
-
while s = src.read(1024)
-
buf << s
-
yield
-
end
-
else # generic reader
-
src.each do |str|
-
buf << str
-
yield if buf.size > 1024
-
end
-
yield unless buf.empty?
-
end
-
end
-
end
-
-
-
#
-
# The writer adapter class
-
#
-
1
class WriteAdapter
-
1
def initialize(socket, method)
-
@socket = socket
-
@method_id = method
-
end
-
-
1
def inspect
-
"#<#{self.class} socket=#{@socket.inspect}>"
-
end
-
-
1
def write(str)
-
@socket.__send__(@method_id, str)
-
end
-
-
1
alias print write
-
-
1
def <<(str)
-
write str
-
self
-
end
-
-
1
def puts(str = '')
-
write str.chomp("\n") + "\n"
-
end
-
-
1
def printf(*args)
-
write sprintf(*args)
-
end
-
end
-
-
-
1
class ReadAdapter #:nodoc: internal use only
-
1
def initialize(block)
-
@block = block
-
end
-
-
1
def inspect
-
"#<#{self.class}>"
-
end
-
-
1
def <<(str)
-
call_block(str, &@block) if @block
-
end
-
-
1
private
-
-
# This method is needed because @block must be called by yield,
-
# not Proc#call. You can see difference when using `break' in
-
# the block.
-
1
def call_block(str)
-
yield str
-
end
-
end
-
-
-
1
module NetPrivate #:nodoc: obsolete
-
1
Socket = ::Net::InternetMessageIO
-
end
-
-
end # module Net
-
# frozen_string_literal: false
-
=begin
-
= Info
-
'OpenSSL for Ruby 2' project
-
Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz>
-
All rights reserved.
-
-
= Licence
-
This program is licensed under the same licence as Ruby.
-
(See the file 'LICENCE'.)
-
=end
-
-
1
require 'openssl.so'
-
-
1
require 'openssl/bn'
-
1
require 'openssl/pkey'
-
1
require 'openssl/cipher'
-
1
require 'openssl/config'
-
1
require 'openssl/digest'
-
1
require 'openssl/x509'
-
1
require 'openssl/ssl'
-
1
require 'openssl/pkcs5'
-
# frozen_string_literal: false
-
#--
-
#
-
# = Ruby-space definitions that completes C-space funcs for BN
-
#
-
# = Info
-
# 'OpenSSL for Ruby 2' project
-
# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz>
-
# All rights reserved.
-
#
-
# = Licence
-
# This program is licensed under the same licence as Ruby.
-
# (See the file 'LICENCE'.)
-
#++
-
-
1
module OpenSSL
-
1
class BN
-
1
include Comparable
-
-
1
def pretty_print(q)
-
q.object_group(self) {
-
q.text ' '
-
q.text to_i.to_s
-
}
-
end
-
end # BN
-
end # OpenSSL
-
-
##
-
#--
-
# Add double dispatch to Integer
-
#++
-
1
class Integer
-
# Casts an Integer as an OpenSSL::BN
-
#
-
# See `man bn` for more info.
-
1
def to_bn
-
OpenSSL::BN::new(self)
-
end
-
end # Integer
-
# coding: binary
-
# frozen_string_literal: false
-
#--
-
#= Info
-
# 'OpenSSL for Ruby 2' project
-
# Copyright (C) 2001 GOTOU YUUZOU <gotoyuzo@notwork.org>
-
# All rights reserved.
-
#
-
#= Licence
-
# This program is licensed under the same licence as Ruby.
-
# (See the file 'LICENCE'.)
-
#++
-
-
##
-
# OpenSSL IO buffering mix-in module.
-
#
-
# This module allows an OpenSSL::SSL::SSLSocket to behave like an IO.
-
#
-
# You typically won't use this module directly, you can see it implemented in
-
# OpenSSL::SSL::SSLSocket.
-
-
1
module OpenSSL::Buffering
-
1
include Enumerable
-
-
##
-
# The "sync mode" of the SSLSocket.
-
#
-
# See IO#sync for full details.
-
-
1
attr_accessor :sync
-
-
##
-
# Default size to read from or write to the SSLSocket for buffer operations.
-
-
1
BLOCK_SIZE = 1024*16
-
-
##
-
# Creates an instance of OpenSSL's buffering IO module.
-
-
1
def initialize(*)
-
super
-
@eof = false
-
@rbuffer = ""
-
@sync = @io.sync
-
end
-
-
#
-
# for reading.
-
#
-
1
private
-
-
##
-
# Fills the buffer from the underlying SSLSocket
-
-
1
def fill_rbuff
-
begin
-
@rbuffer << self.sysread(BLOCK_SIZE)
-
rescue Errno::EAGAIN
-
retry
-
rescue EOFError
-
@eof = true
-
end
-
end
-
-
##
-
# Consumes _size_ bytes from the buffer
-
-
1
def consume_rbuff(size=nil)
-
if @rbuffer.empty?
-
nil
-
else
-
size = @rbuffer.size unless size
-
ret = @rbuffer[0, size]
-
@rbuffer[0, size] = ""
-
ret
-
end
-
end
-
-
1
public
-
-
##
-
# Reads _size_ bytes from the stream. If _buf_ is provided it must
-
# reference a string which will receive the data.
-
#
-
# See IO#read for full details.
-
-
1
def read(size=nil, buf=nil)
-
if size == 0
-
if buf
-
buf.clear
-
return buf
-
else
-
return ""
-
end
-
end
-
until @eof
-
break if size && size <= @rbuffer.size
-
fill_rbuff
-
end
-
ret = consume_rbuff(size) || ""
-
if buf
-
buf.replace(ret)
-
ret = buf
-
end
-
(size && ret.empty?) ? nil : ret
-
end
-
-
##
-
# Reads at most _maxlen_ bytes from the stream. If _buf_ is provided it
-
# must reference a string which will receive the data.
-
#
-
# See IO#readpartial for full details.
-
-
1
def readpartial(maxlen, buf=nil)
-
if maxlen == 0
-
if buf
-
buf.clear
-
return buf
-
else
-
return ""
-
end
-
end
-
if @rbuffer.empty?
-
begin
-
return sysread(maxlen, buf)
-
rescue Errno::EAGAIN
-
retry
-
end
-
end
-
ret = consume_rbuff(maxlen)
-
if buf
-
buf.replace(ret)
-
ret = buf
-
end
-
ret
-
end
-
-
##
-
# Reads at most _maxlen_ bytes in the non-blocking manner.
-
#
-
# When no data can be read without blocking it raises
-
# OpenSSL::SSL::SSLError extended by IO::WaitReadable or IO::WaitWritable.
-
#
-
# IO::WaitReadable means SSL needs to read internally so read_nonblock
-
# should be called again when the underlying IO is readable.
-
#
-
# IO::WaitWritable means SSL needs to write internally so read_nonblock
-
# should be called again after the underlying IO is writable.
-
#
-
# OpenSSL::Buffering#read_nonblock needs two rescue clause as follows:
-
#
-
# # emulates blocking read (readpartial).
-
# begin
-
# result = ssl.read_nonblock(maxlen)
-
# rescue IO::WaitReadable
-
# IO.select([io])
-
# retry
-
# rescue IO::WaitWritable
-
# IO.select(nil, [io])
-
# retry
-
# end
-
#
-
# Note that one reason that read_nonblock writes to the underlying IO is
-
# when the peer requests a new TLS/SSL handshake. See openssl the FAQ for
-
# more details. http://www.openssl.org/support/faq.html
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that read_nonblock should not raise an IO::Wait*able exception, but
-
# return the symbol +:wait_writable+ or +:wait_readable+ instead. At EOF,
-
# it will return +nil+ instead of raising EOFError.
-
-
1
def read_nonblock(maxlen, buf=nil, exception: true)
-
if maxlen == 0
-
if buf
-
buf.clear
-
return buf
-
else
-
return ""
-
end
-
end
-
if @rbuffer.empty?
-
return sysread_nonblock(maxlen, buf, exception: exception)
-
end
-
ret = consume_rbuff(maxlen)
-
if buf
-
buf.replace(ret)
-
ret = buf
-
end
-
ret
-
end
-
-
##
-
# Reads the next "line" from the stream. Lines are separated by _eol_. If
-
# _limit_ is provided the result will not be longer than the given number of
-
# bytes.
-
#
-
# _eol_ may be a String or Regexp.
-
#
-
# Unlike IO#gets the line read will not be assigned to +$_+.
-
#
-
# Unlike IO#gets the separator must be provided if a limit is provided.
-
-
1
def gets(eol=$/, limit=nil)
-
idx = @rbuffer.index(eol)
-
until @eof
-
break if idx
-
fill_rbuff
-
idx = @rbuffer.index(eol)
-
end
-
if eol.is_a?(Regexp)
-
size = idx ? idx+$&.size : nil
-
else
-
size = idx ? idx+eol.size : nil
-
end
-
if size && limit && limit >= 0
-
size = [size, limit].min
-
end
-
consume_rbuff(size)
-
end
-
-
##
-
# Executes the block for every line in the stream where lines are separated
-
# by _eol_.
-
#
-
# See also #gets
-
-
1
def each(eol=$/)
-
while line = self.gets(eol)
-
yield line
-
end
-
end
-
1
alias each_line each
-
-
##
-
# Reads lines from the stream which are separated by _eol_.
-
#
-
# See also #gets
-
-
1
def readlines(eol=$/)
-
ary = []
-
while line = self.gets(eol)
-
ary << line
-
end
-
ary
-
end
-
-
##
-
# Reads a line from the stream which is separated by _eol_.
-
#
-
# Raises EOFError if at end of file.
-
-
1
def readline(eol=$/)
-
raise EOFError if eof?
-
gets(eol)
-
end
-
-
##
-
# Reads one character from the stream. Returns nil if called at end of
-
# file.
-
-
1
def getc
-
read(1)
-
end
-
-
##
-
# Calls the given block once for each byte in the stream.
-
-
1
def each_byte # :yields: byte
-
while c = getc
-
yield(c.ord)
-
end
-
end
-
-
##
-
# Reads a one-character string from the stream. Raises an EOFError at end
-
# of file.
-
-
1
def readchar
-
raise EOFError if eof?
-
getc
-
end
-
-
##
-
# Pushes character _c_ back onto the stream such that a subsequent buffered
-
# character read will return it.
-
#
-
# Unlike IO#getc multiple bytes may be pushed back onto the stream.
-
#
-
# Has no effect on unbuffered reads (such as #sysread).
-
-
1
def ungetc(c)
-
@rbuffer[0,0] = c.chr
-
end
-
-
##
-
# Returns true if the stream is at file which means there is no more data to
-
# be read.
-
-
1
def eof?
-
fill_rbuff if !@eof && @rbuffer.empty?
-
@eof && @rbuffer.empty?
-
end
-
1
alias eof eof?
-
-
#
-
# for writing.
-
#
-
1
private
-
-
##
-
# Writes _s_ to the buffer. When the buffer is full or #sync is true the
-
# buffer is flushed to the underlying socket.
-
-
1
def do_write(s)
-
@wbuffer = "" unless defined? @wbuffer
-
@wbuffer << s
-
@wbuffer.force_encoding(Encoding::BINARY)
-
@sync ||= false
-
if @sync or @wbuffer.size > BLOCK_SIZE
-
until @wbuffer.empty?
-
begin
-
nwrote = syswrite(@wbuffer)
-
rescue Errno::EAGAIN
-
retry
-
end
-
@wbuffer[0, nwrote] = ""
-
end
-
end
-
end
-
-
1
public
-
-
##
-
# Writes _s_ to the stream. If the argument is not a String it will be
-
# converted using +.to_s+ method. Returns the number of bytes written.
-
-
1
def write(*s)
-
s.inject(0) do |written, str|
-
do_write(str)
-
written + str.bytesize
-
end
-
end
-
-
##
-
# Writes _s_ in the non-blocking manner.
-
#
-
# If there is buffered data, it is flushed first. This may block.
-
#
-
# write_nonblock returns number of bytes written to the SSL connection.
-
#
-
# When no data can be written without blocking it raises
-
# OpenSSL::SSL::SSLError extended by IO::WaitReadable or IO::WaitWritable.
-
#
-
# IO::WaitReadable means SSL needs to read internally so write_nonblock
-
# should be called again after the underlying IO is readable.
-
#
-
# IO::WaitWritable means SSL needs to write internally so write_nonblock
-
# should be called again after underlying IO is writable.
-
#
-
# So OpenSSL::Buffering#write_nonblock needs two rescue clause as follows.
-
#
-
# # emulates blocking write.
-
# begin
-
# result = ssl.write_nonblock(str)
-
# rescue IO::WaitReadable
-
# IO.select([io])
-
# retry
-
# rescue IO::WaitWritable
-
# IO.select(nil, [io])
-
# retry
-
# end
-
#
-
# Note that one reason that write_nonblock reads from the underlying IO
-
# is when the peer requests a new TLS/SSL handshake. See the openssl FAQ
-
# for more details. http://www.openssl.org/support/faq.html
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that write_nonblock should not raise an IO::Wait*able exception, but
-
# return the symbol +:wait_writable+ or +:wait_readable+ instead.
-
-
1
def write_nonblock(s, exception: true)
-
flush
-
syswrite_nonblock(s, exception: exception)
-
end
-
-
##
-
# Writes _s_ to the stream. _s_ will be converted to a String using
-
# +.to_s+ method.
-
-
1
def <<(s)
-
do_write(s)
-
self
-
end
-
-
##
-
# Writes _args_ to the stream along with a record separator.
-
#
-
# See IO#puts for full details.
-
-
1
def puts(*args)
-
s = ""
-
if args.empty?
-
s << "\n"
-
end
-
args.each{|arg|
-
s << arg.to_s
-
s.sub!(/(?<!\n)\z/, "\n")
-
}
-
do_write(s)
-
nil
-
end
-
-
##
-
# Writes _args_ to the stream.
-
#
-
# See IO#print for full details.
-
-
1
def print(*args)
-
s = ""
-
args.each{ |arg| s << arg.to_s }
-
do_write(s)
-
nil
-
end
-
-
##
-
# Formats and writes to the stream converting parameters under control of
-
# the format string.
-
#
-
# See Kernel#sprintf for format string details.
-
-
1
def printf(s, *args)
-
do_write(s % args)
-
nil
-
end
-
-
##
-
# Flushes buffered data to the SSLSocket.
-
-
1
def flush
-
osync = @sync
-
@sync = true
-
do_write ""
-
return self
-
ensure
-
@sync = osync
-
end
-
-
##
-
# Closes the SSLSocket and flushes any unwritten data.
-
-
1
def close
-
flush rescue nil
-
sysclose
-
end
-
end
-
# frozen_string_literal: false
-
#--
-
# = Ruby-space predefined Cipher subclasses
-
#
-
# = Info
-
# 'OpenSSL for Ruby 2' project
-
# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz>
-
# All rights reserved.
-
#
-
# = Licence
-
# This program is licensed under the same licence as Ruby.
-
# (See the file 'LICENCE'.)
-
#++
-
-
1
module OpenSSL
-
1
class Cipher
-
1
%w(AES CAST5 BF DES IDEA RC2 RC4 RC5).each{|name|
-
8
klass = Class.new(Cipher){
-
8
define_method(:initialize){|*args|
-
cipher_name = args.inject(name){|n, arg| "#{n}-#{arg}" }
-
super(cipher_name.downcase)
-
}
-
}
-
8
const_set(name, klass)
-
}
-
-
1
%w(128 192 256).each{|keylen|
-
3
klass = Class.new(Cipher){
-
3
define_method(:initialize){|mode = "CBC"|
-
super("aes-#{keylen}-#{mode}".downcase)
-
}
-
}
-
3
const_set("AES#{keylen}", klass)
-
}
-
-
# call-seq:
-
# cipher.random_key -> key
-
#
-
# Generate a random key with OpenSSL::Random.random_bytes and sets it to
-
# the cipher, and returns it.
-
#
-
# You must call #encrypt or #decrypt before calling this method.
-
1
def random_key
-
str = OpenSSL::Random.random_bytes(self.key_len)
-
self.key = str
-
end
-
-
# call-seq:
-
# cipher.random_iv -> iv
-
#
-
# Generate a random IV with OpenSSL::Random.random_bytes and sets it to the
-
# cipher, and returns it.
-
#
-
# You must call #encrypt or #decrypt before calling this method.
-
1
def random_iv
-
str = OpenSSL::Random.random_bytes(self.iv_len)
-
self.iv = str
-
end
-
-
# Deprecated.
-
#
-
# This class is only provided for backwards compatibility.
-
# Use OpenSSL::Cipher.
-
1
class Cipher < Cipher; end
-
1
deprecate_constant :Cipher
-
end # Cipher
-
end # OpenSSL
-
# frozen_string_literal: false
-
=begin
-
= Ruby-space definitions that completes C-space funcs for Config
-
-
= Info
-
Copyright (C) 2010 Hiroshi Nakamura <nahi@ruby-lang.org>
-
-
= Licence
-
This program is licensed under the same licence as Ruby.
-
(See the file 'LICENCE'.)
-
-
=end
-
-
1
require 'stringio'
-
-
1
module OpenSSL
-
##
-
# = OpenSSL::Config
-
#
-
# Configuration for the openssl library.
-
#
-
# Many system's installation of openssl library will depend on your system
-
# configuration. See the value of OpenSSL::Config::DEFAULT_CONFIG_FILE for
-
# the location of the file for your host.
-
#
-
# See also http://www.openssl.org/docs/apps/config.html
-
1
class Config
-
1
include Enumerable
-
-
1
class << self
-
-
##
-
# Parses a given _string_ as a blob that contains configuration for
-
# OpenSSL.
-
#
-
# If the source of the IO is a file, then consider using #parse_config.
-
1
def parse(string)
-
c = new()
-
parse_config(StringIO.new(string)).each do |section, hash|
-
c[section] = hash
-
end
-
c
-
end
-
-
##
-
# load is an alias to ::new
-
1
alias load new
-
-
##
-
# Parses the configuration data read from _io_, see also #parse.
-
#
-
# Raises a ConfigError on invalid configuration data.
-
1
def parse_config(io)
-
begin
-
parse_config_lines(io)
-
rescue ConfigError => e
-
e.message.replace("error in line #{io.lineno}: " + e.message)
-
raise
-
end
-
end
-
-
1
def get_key_string(data, section, key) # :nodoc:
-
if v = data[section] && data[section][key]
-
return v
-
elsif section == 'ENV'
-
if v = ENV[key]
-
return v
-
end
-
end
-
if v = data['default'] && data['default'][key]
-
return v
-
end
-
end
-
-
1
private
-
-
1
def parse_config_lines(io)
-
section = 'default'
-
data = {section => {}}
-
while definition = get_definition(io)
-
definition = clear_comments(definition)
-
next if definition.empty?
-
if definition[0] == ?[
-
if /\[([^\]]*)\]/ =~ definition
-
section = $1.strip
-
data[section] ||= {}
-
else
-
raise ConfigError, "missing close square bracket"
-
end
-
else
-
if /\A([^:\s]*)(?:::([^:\s]*))?\s*=(.*)\z/ =~ definition
-
if $2
-
section = $1
-
key = $2
-
else
-
key = $1
-
end
-
value = unescape_value(data, section, $3)
-
(data[section] ||= {})[key] = value.strip
-
else
-
raise ConfigError, "missing equal sign"
-
end
-
end
-
end
-
data
-
end
-
-
# escape with backslash
-
1
QUOTE_REGEXP_SQ = /\A([^'\\]*(?:\\.[^'\\]*)*)'/
-
# escape with backslash and doubled dq
-
1
QUOTE_REGEXP_DQ = /\A([^"\\]*(?:""[^"\\]*|\\.[^"\\]*)*)"/
-
# escaped char map
-
ESCAPE_MAP = {
-
1
"r" => "\r",
-
"n" => "\n",
-
"b" => "\b",
-
"t" => "\t",
-
}
-
-
1
def unescape_value(data, section, value)
-
scanned = []
-
while m = value.match(/['"\\$]/)
-
scanned << m.pre_match
-
c = m[0]
-
value = m.post_match
-
case c
-
when "'"
-
if m = value.match(QUOTE_REGEXP_SQ)
-
scanned << m[1].gsub(/\\(.)/, '\\1')
-
value = m.post_match
-
else
-
break
-
end
-
when '"'
-
if m = value.match(QUOTE_REGEXP_DQ)
-
scanned << m[1].gsub(/""/, '').gsub(/\\(.)/, '\\1')
-
value = m.post_match
-
else
-
break
-
end
-
when "\\"
-
c = value.slice!(0, 1)
-
scanned << (ESCAPE_MAP[c] || c)
-
when "$"
-
ref, value = extract_reference(value)
-
refsec = section
-
if ref.index('::')
-
refsec, ref = ref.split('::', 2)
-
end
-
if v = get_key_string(data, refsec, ref)
-
scanned << v
-
else
-
raise ConfigError, "variable has no value"
-
end
-
else
-
raise 'must not reaced'
-
end
-
end
-
scanned << value
-
scanned.join
-
end
-
-
1
def extract_reference(value)
-
rest = ''
-
if m = value.match(/\(([^)]*)\)|\{([^}]*)\}/)
-
value = m[1] || m[2]
-
rest = m.post_match
-
elsif [?(, ?{].include?(value[0])
-
raise ConfigError, "no close brace"
-
end
-
if m = value.match(/[a-zA-Z0-9_]*(?:::[a-zA-Z0-9_]*)?/)
-
return m[0], m.post_match + rest
-
else
-
raise
-
end
-
end
-
-
1
def clear_comments(line)
-
# FCOMMENT
-
if m = line.match(/\A([\t\n\f ]*);.*\z/)
-
return m[1]
-
end
-
# COMMENT
-
scanned = []
-
while m = line.match(/[#'"\\]/)
-
scanned << m.pre_match
-
c = m[0]
-
line = m.post_match
-
case c
-
when '#'
-
line = nil
-
break
-
when "'", '"'
-
regexp = (c == "'") ? QUOTE_REGEXP_SQ : QUOTE_REGEXP_DQ
-
scanned << c
-
if m = line.match(regexp)
-
scanned << m[0]
-
line = m.post_match
-
else
-
scanned << line
-
line = nil
-
break
-
end
-
when "\\"
-
scanned << c
-
scanned << line.slice!(0, 1)
-
else
-
raise 'must not reaced'
-
end
-
end
-
scanned << line
-
scanned.join
-
end
-
-
1
def get_definition(io)
-
if line = get_line(io)
-
while /[^\\]\\\z/ =~ line
-
if extra = get_line(io)
-
line += extra
-
else
-
break
-
end
-
end
-
return line.strip
-
end
-
end
-
-
1
def get_line(io)
-
if line = io.gets
-
line.gsub(/[\r\n]*/, '')
-
end
-
end
-
end
-
-
##
-
# Creates an instance of OpenSSL's configuration class.
-
#
-
# This can be used in contexts like OpenSSL::X509::ExtensionFactory.config=
-
#
-
# If the optional _filename_ parameter is provided, then it is read in and
-
# parsed via #parse_config.
-
#
-
# This can raise IO exceptions based on the access, or availability of the
-
# file. A ConfigError exception may be raised depending on the validity of
-
# the data being configured.
-
#
-
1
def initialize(filename = nil)
-
@data = {}
-
if filename
-
File.open(filename.to_s) do |file|
-
Config.parse_config(file).each do |section, hash|
-
self[section] = hash
-
end
-
end
-
end
-
end
-
-
##
-
# Gets the value of _key_ from the given _section_
-
#
-
# Given the following configurating file being loaded:
-
#
-
# config = OpenSSL::Config.load('foo.cnf')
-
# #=> #<OpenSSL::Config sections=["default"]>
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=bar
-
#
-
# You can get a specific value from the config if you know the _section_
-
# and _key_ like so:
-
#
-
# config.get_value('default','foo')
-
# #=> "bar"
-
#
-
1
def get_value(section, key)
-
if section.nil?
-
raise TypeError.new('nil not allowed')
-
end
-
section = 'default' if section.empty?
-
get_key_string(section, key)
-
end
-
-
##
-
#
-
# *Deprecated*
-
#
-
# Use #get_value instead
-
1
def value(arg1, arg2 = nil) # :nodoc:
-
warn('Config#value is deprecated; use Config#get_value')
-
if arg2.nil?
-
section, key = 'default', arg1
-
else
-
section, key = arg1, arg2
-
end
-
section ||= 'default'
-
section = 'default' if section.empty?
-
get_key_string(section, key)
-
end
-
-
##
-
# Set the target _key_ with a given _value_ under a specific _section_.
-
#
-
# Given the following configurating file being loaded:
-
#
-
# config = OpenSSL::Config.load('foo.cnf')
-
# #=> #<OpenSSL::Config sections=["default"]>
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=bar
-
#
-
# You can set the value of _foo_ under the _default_ section to a new
-
# value:
-
#
-
# config.add_value('default', 'foo', 'buzz')
-
# #=> "buzz"
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=buzz
-
#
-
1
def add_value(section, key, value)
-
check_modify
-
(@data[section] ||= {})[key] = value
-
end
-
-
##
-
# Get a specific _section_ from the current configuration
-
#
-
# Given the following configurating file being loaded:
-
#
-
# config = OpenSSL::Config.load('foo.cnf')
-
# #=> #<OpenSSL::Config sections=["default"]>
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=bar
-
#
-
# You can get a hash of the specific section like so:
-
#
-
# config['default']
-
# #=> {"foo"=>"bar"}
-
#
-
1
def [](section)
-
@data[section] || {}
-
end
-
-
##
-
# Deprecated
-
#
-
# Use #[] instead
-
1
def section(name) # :nodoc:
-
warn('Config#section is deprecated; use Config#[]')
-
@data[name] || {}
-
end
-
-
##
-
# Sets a specific _section_ name with a Hash _pairs_.
-
#
-
# Given the following configuration being created:
-
#
-
# config = OpenSSL::Config.new
-
# #=> #<OpenSSL::Config sections=[]>
-
# config['default'] = {"foo"=>"bar","baz"=>"buz"}
-
# #=> {"foo"=>"bar", "baz"=>"buz"}
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=bar
-
# # baz=buz
-
#
-
# It's important to note that this will essentially merge any of the keys
-
# in _pairs_ with the existing _section_. For example:
-
#
-
# config['default']
-
# #=> {"foo"=>"bar", "baz"=>"buz"}
-
# config['default'] = {"foo" => "changed"}
-
# #=> {"foo"=>"changed"}
-
# config['default']
-
# #=> {"foo"=>"changed", "baz"=>"buz"}
-
#
-
1
def []=(section, pairs)
-
check_modify
-
@data[section] ||= {}
-
pairs.each do |key, value|
-
self.add_value(section, key, value)
-
end
-
end
-
-
##
-
# Get the names of all sections in the current configuration
-
1
def sections
-
@data.keys
-
end
-
-
##
-
# Get the parsable form of the current configuration
-
#
-
# Given the following configuration being created:
-
#
-
# config = OpenSSL::Config.new
-
# #=> #<OpenSSL::Config sections=[]>
-
# config['default'] = {"foo"=>"bar","baz"=>"buz"}
-
# #=> {"foo"=>"bar", "baz"=>"buz"}
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=bar
-
# # baz=buz
-
#
-
# You can parse get the serialized configuration using #to_s and then parse
-
# it later:
-
#
-
# serialized_config = config.to_s
-
# # much later...
-
# new_config = OpenSSL::Config.parse(serialized_config)
-
# #=> #<OpenSSL::Config sections=["default"]>
-
# puts new_config
-
# #=> [ default ]
-
# foo=bar
-
# baz=buz
-
#
-
1
def to_s
-
ary = []
-
@data.keys.sort.each do |section|
-
ary << "[ #{section} ]\n"
-
@data[section].keys.each do |key|
-
ary << "#{key}=#{@data[section][key]}\n"
-
end
-
ary << "\n"
-
end
-
ary.join
-
end
-
-
##
-
# For a block.
-
#
-
# Receive the section and its pairs for the current configuration.
-
#
-
# config.each do |section, key, value|
-
# # ...
-
# end
-
#
-
1
def each
-
@data.each do |section, hash|
-
hash.each do |key, value|
-
yield [section, key, value]
-
end
-
end
-
end
-
-
##
-
# String representation of this configuration object, including the class
-
# name and its sections.
-
1
def inspect
-
"#<#{self.class.name} sections=#{sections.inspect}>"
-
end
-
-
1
protected
-
-
1
def data # :nodoc:
-
@data
-
end
-
-
1
private
-
-
1
def initialize_copy(other)
-
@data = other.data.dup
-
end
-
-
1
def check_modify
-
raise TypeError.new("Insecure: can't modify OpenSSL config") if frozen?
-
end
-
-
1
def get_key_string(section, key)
-
Config.get_key_string(@data, section, key)
-
end
-
end
-
end
-
# frozen_string_literal: false
-
#--
-
# = Ruby-space predefined Digest subclasses
-
#
-
# = Info
-
# 'OpenSSL for Ruby 2' project
-
# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz>
-
# All rights reserved.
-
#
-
# = Licence
-
# This program is licensed under the same licence as Ruby.
-
# (See the file 'LICENCE'.)
-
#++
-
-
1
module OpenSSL
-
1
class Digest
-
-
1
alg = %w(MD2 MD4 MD5 MDC2 RIPEMD160 SHA1 SHA224 SHA256 SHA384 SHA512)
-
1
if OPENSSL_VERSION_NUMBER < 0x10100000
-
alg += %w(DSS DSS1 SHA)
-
end
-
-
# Return the hash value computed with _name_ Digest. _name_ is either the
-
# long name or short name of a supported digest algorithm.
-
#
-
# === Examples
-
#
-
# OpenSSL::Digest.digest("SHA256", "abc")
-
#
-
# which is equivalent to:
-
#
-
# OpenSSL::Digest::SHA256.digest("abc")
-
-
1
def self.digest(name, data)
-
super(data, name)
-
end
-
-
1
alg.each{|name|
-
10
klass = Class.new(self) {
-
10
define_method(:initialize, ->(data = nil) {super(name, data)})
-
}
-
20
singleton = (class << klass; self; end)
-
10
singleton.class_eval{
-
10
define_method(:digest){|data| new.digest(data) }
-
10
define_method(:hexdigest){|data| new.hexdigest(data) }
-
}
-
10
const_set(name, klass)
-
}
-
-
# Deprecated.
-
#
-
# This class is only provided for backwards compatibility.
-
# Use OpenSSL::Digest instead.
-
1
class Digest < Digest; end # :nodoc:
-
1
deprecate_constant :Digest
-
-
end # Digest
-
-
# Returns a Digest subclass by _name_
-
#
-
# require 'openssl'
-
#
-
# OpenSSL::Digest("MD5")
-
# # => OpenSSL::Digest::MD5
-
#
-
# Digest("Foo")
-
# # => NameError: wrong constant name Foo
-
-
1
def Digest(name)
-
OpenSSL::Digest.const_get(name)
-
end
-
-
1
module_function :Digest
-
-
end # OpenSSL
-
# frozen_string_literal: false
-
#--
-
# Ruby/OpenSSL Project
-
# Copyright (C) 2017 Ruby/OpenSSL Project Authors
-
#++
-
-
1
module OpenSSL
-
1
module PKCS5
-
1
module_function
-
-
# OpenSSL::PKCS5.pbkdf2_hmac has been renamed to OpenSSL::KDF.pbkdf2_hmac.
-
# This method is provided for backwards compatibility.
-
1
def pbkdf2_hmac(pass, salt, iter, keylen, digest)
-
OpenSSL::KDF.pbkdf2_hmac(pass, salt: salt, iterations: iter,
-
length: keylen, hash: digest)
-
end
-
-
1
def pbkdf2_hmac_sha1(pass, salt, iter, keylen)
-
pbkdf2_hmac(pass, salt, iter, keylen, "sha1")
-
end
-
end
-
end
-
# frozen_string_literal: false
-
#--
-
# Ruby/OpenSSL Project
-
# Copyright (C) 2017 Ruby/OpenSSL Project Authors
-
#++
-
-
1
module OpenSSL::PKey
-
1
if defined?(EC)
-
1
class EC::Point
-
# :call-seq:
-
# point.to_bn([conversion_form]) -> OpenSSL::BN
-
#
-
# Returns the octet string representation of the EC point as an instance of
-
# OpenSSL::BN.
-
#
-
# If _conversion_form_ is not given, the _point_conversion_form_ attribute
-
# set to the group is used.
-
#
-
# See #to_octet_string for more information.
-
1
def to_bn(conversion_form = group.point_conversion_form)
-
OpenSSL::BN.new(to_octet_string(conversion_form), 2)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: false
-
=begin
-
= Info
-
'OpenSSL for Ruby 2' project
-
Copyright (C) 2001 GOTOU YUUZOU <gotoyuzo@notwork.org>
-
All rights reserved.
-
-
= Licence
-
This program is licensed under the same licence as Ruby.
-
(See the file 'LICENCE'.)
-
=end
-
-
1
require "openssl/buffering"
-
1
require "io/nonblock"
-
1
require "ipaddr"
-
-
1
module OpenSSL
-
1
module SSL
-
1
class SSLContext
-
DEFAULT_PARAMS = { # :nodoc:
-
1
:min_version => OpenSSL::SSL::TLS1_VERSION,
-
:verify_mode => OpenSSL::SSL::VERIFY_PEER,
-
:verify_hostname => true,
-
:options => -> {
-
1
opts = OpenSSL::SSL::OP_ALL
-
1
opts &= ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS
-
1
opts |= OpenSSL::SSL::OP_NO_COMPRESSION
-
1
opts
-
}.call
-
}
-
-
1
if defined?(OpenSSL::PKey::DH)
-
1
DEFAULT_2048 = OpenSSL::PKey::DH.new <<-_end_of_pem_
-
-----BEGIN DH PARAMETERS-----
-
MIIBCAKCAQEA7E6kBrYiyvmKAMzQ7i8WvwVk9Y/+f8S7sCTN712KkK3cqd1jhJDY
-
JbrYeNV3kUIKhPxWHhObHKpD1R84UpL+s2b55+iMd6GmL7OYmNIT/FccKhTcveab
-
VBmZT86BZKYyf45hUF9FOuUM9xPzuK3Vd8oJQvfYMCd7LPC0taAEljQLR4Edf8E6
-
YoaOffgTf5qxiwkjnlVZQc3whgnEt9FpVMvQ9eknyeGB5KHfayAc3+hUAvI3/Cr3
-
1bNveX5wInh5GDx1FGhKBZ+s1H+aedudCm7sCgRwv8lKWYGiHzObSma8A86KG+MD
-
7Lo5JquQ3DlBodj3IDyPrxIv96lvRPFtAwIBAg==
-
-----END DH PARAMETERS-----
-
_end_of_pem_
-
1
private_constant :DEFAULT_2048
-
-
1
DEFAULT_TMP_DH_CALLBACK = lambda { |ctx, is_export, keylen| # :nodoc:
-
warn "using default DH parameters." if $VERBOSE
-
DEFAULT_2048
-
}
-
end
-
-
1
if !(OpenSSL::OPENSSL_VERSION.start_with?("OpenSSL") &&
-
OpenSSL::OPENSSL_VERSION_NUMBER >= 0x10100000)
-
DEFAULT_PARAMS.merge!(
-
ciphers: %w{
-
ECDHE-ECDSA-AES128-GCM-SHA256
-
ECDHE-RSA-AES128-GCM-SHA256
-
ECDHE-ECDSA-AES256-GCM-SHA384
-
ECDHE-RSA-AES256-GCM-SHA384
-
DHE-RSA-AES128-GCM-SHA256
-
DHE-DSS-AES128-GCM-SHA256
-
DHE-RSA-AES256-GCM-SHA384
-
DHE-DSS-AES256-GCM-SHA384
-
ECDHE-ECDSA-AES128-SHA256
-
ECDHE-RSA-AES128-SHA256
-
ECDHE-ECDSA-AES128-SHA
-
ECDHE-RSA-AES128-SHA
-
ECDHE-ECDSA-AES256-SHA384
-
ECDHE-RSA-AES256-SHA384
-
ECDHE-ECDSA-AES256-SHA
-
ECDHE-RSA-AES256-SHA
-
DHE-RSA-AES128-SHA256
-
DHE-RSA-AES256-SHA256
-
DHE-RSA-AES128-SHA
-
DHE-RSA-AES256-SHA
-
DHE-DSS-AES128-SHA256
-
DHE-DSS-AES256-SHA256
-
DHE-DSS-AES128-SHA
-
DHE-DSS-AES256-SHA
-
AES128-GCM-SHA256
-
AES256-GCM-SHA384
-
AES128-SHA256
-
AES256-SHA256
-
AES128-SHA
-
AES256-SHA
-
}.join(":"),
-
)
-
end
-
-
1
DEFAULT_CERT_STORE = OpenSSL::X509::Store.new # :nodoc:
-
1
DEFAULT_CERT_STORE.set_default_paths
-
1
DEFAULT_CERT_STORE.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
-
-
# A callback invoked when DH parameters are required.
-
#
-
# The callback is invoked with the Session for the key exchange, an
-
# flag indicating the use of an export cipher and the keylength
-
# required.
-
#
-
# The callback must return an OpenSSL::PKey::DH instance of the correct
-
# key length.
-
-
1
attr_accessor :tmp_dh_callback
-
-
# A callback invoked at connect time to distinguish between multiple
-
# server names.
-
#
-
# The callback is invoked with an SSLSocket and a server name. The
-
# callback must return an SSLContext for the server name or nil.
-
1
attr_accessor :servername_cb
-
-
# call-seq:
-
# SSLContext.new -> ctx
-
# SSLContext.new(:TLSv1) -> ctx
-
# SSLContext.new("SSLv23") -> ctx
-
#
-
# Creates a new SSL context.
-
#
-
# If an argument is given, #ssl_version= is called with the value. Note
-
# that this form is deprecated. New applications should use #min_version=
-
# and #max_version= as necessary.
-
1
def initialize(version = nil)
-
self.options |= OpenSSL::SSL::OP_ALL
-
self.ssl_version = version if version
-
end
-
-
##
-
# call-seq:
-
# ctx.set_params(params = {}) -> params
-
#
-
# Sets saner defaults optimized for the use with HTTP-like protocols.
-
#
-
# If a Hash _params_ is given, the parameters are overridden with it.
-
# The keys in _params_ must be assignment methods on SSLContext.
-
#
-
# If the verify_mode is not VERIFY_NONE and ca_file, ca_path and
-
# cert_store are not set then the system default certificate store is
-
# used.
-
1
def set_params(params={})
-
params = DEFAULT_PARAMS.merge(params)
-
self.options = params.delete(:options) # set before min_version/max_version
-
params.each{|name, value| self.__send__("#{name}=", value) }
-
if self.verify_mode != OpenSSL::SSL::VERIFY_NONE
-
unless self.ca_file or self.ca_path or self.cert_store
-
self.cert_store = DEFAULT_CERT_STORE
-
end
-
end
-
return params
-
end
-
-
# call-seq:
-
# ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION
-
# ctx.min_version = :TLS1_2
-
# ctx.min_version = nil
-
#
-
# Sets the lower bound on the supported SSL/TLS protocol version. The
-
# version may be specified by an integer constant named
-
# OpenSSL::SSL::*_VERSION, a Symbol, or +nil+ which means "any version".
-
#
-
# Be careful that you don't overwrite OpenSSL::SSL::OP_NO_{SSL,TLS}v*
-
# options by #options= once you have called #min_version= or
-
# #max_version=.
-
#
-
# === Example
-
# ctx = OpenSSL::SSL::SSLContext.new
-
# ctx.min_version = OpenSSL::SSL::TLS1_1_VERSION
-
# ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
-
#
-
# sock = OpenSSL::SSL::SSLSocket.new(tcp_sock, ctx)
-
# sock.connect # Initiates a connection using either TLS 1.1 or TLS 1.2
-
1
def min_version=(version)
-
set_minmax_proto_version(version, @max_proto_version ||= nil)
-
@min_proto_version = version
-
end
-
-
# call-seq:
-
# ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
-
# ctx.max_version = :TLS1_2
-
# ctx.max_version = nil
-
#
-
# Sets the upper bound of the supported SSL/TLS protocol version. See
-
# #min_version= for the possible values.
-
1
def max_version=(version)
-
set_minmax_proto_version(@min_proto_version ||= nil, version)
-
@max_proto_version = version
-
end
-
-
# call-seq:
-
# ctx.ssl_version = :TLSv1
-
# ctx.ssl_version = "SSLv23"
-
#
-
# Sets the SSL/TLS protocol version for the context. This forces
-
# connections to use only the specified protocol version. This is
-
# deprecated and only provided for backwards compatibility. Use
-
# #min_version= and #max_version= instead.
-
#
-
# === History
-
# As the name hints, this used to call the SSL_CTX_set_ssl_version()
-
# function which sets the SSL method used for connections created from
-
# the context. As of Ruby/OpenSSL 2.1, this accessor method is
-
# implemented to call #min_version= and #max_version= instead.
-
1
def ssl_version=(meth)
-
meth = meth.to_s if meth.is_a?(Symbol)
-
if /(?<type>_client|_server)\z/ =~ meth
-
meth = $`
-
if $VERBOSE
-
warn "#{caller(1, 1)[0]}: method type #{type.inspect} is ignored"
-
end
-
end
-
version = METHODS_MAP[meth.intern] or
-
raise ArgumentError, "unknown SSL method `%s'" % meth
-
set_minmax_proto_version(version, version)
-
@min_proto_version = @max_proto_version = version
-
end
-
-
METHODS_MAP = {
-
1
SSLv23: 0,
-
SSLv2: OpenSSL::SSL::SSL2_VERSION,
-
SSLv3: OpenSSL::SSL::SSL3_VERSION,
-
TLSv1: OpenSSL::SSL::TLS1_VERSION,
-
TLSv1_1: OpenSSL::SSL::TLS1_1_VERSION,
-
TLSv1_2: OpenSSL::SSL::TLS1_2_VERSION,
-
}.freeze
-
1
private_constant :METHODS_MAP
-
-
# The list of available SSL/TLS methods. This constant is only provided
-
# for backwards compatibility.
-
1
METHODS = METHODS_MAP.flat_map { |name,|
-
6
[name, :"#{name}_client", :"#{name}_server"]
-
}.freeze
-
1
deprecate_constant :METHODS
-
end
-
-
1
module SocketForwarder
-
1
def addr
-
to_io.addr
-
end
-
-
1
def peeraddr
-
to_io.peeraddr
-
end
-
-
1
def setsockopt(level, optname, optval)
-
to_io.setsockopt(level, optname, optval)
-
end
-
-
1
def getsockopt(level, optname)
-
to_io.getsockopt(level, optname)
-
end
-
-
1
def fcntl(*args)
-
to_io.fcntl(*args)
-
end
-
-
1
def closed?
-
to_io.closed?
-
end
-
-
1
def do_not_reverse_lookup=(flag)
-
to_io.do_not_reverse_lookup = flag
-
end
-
end
-
-
1
def verify_certificate_identity(cert, hostname)
-
should_verify_common_name = true
-
cert.extensions.each{|ext|
-
next if ext.oid != "subjectAltName"
-
ostr = OpenSSL::ASN1.decode(ext.to_der).value.last
-
sequence = OpenSSL::ASN1.decode(ostr.value)
-
sequence.value.each{|san|
-
case san.tag
-
when 2 # dNSName in GeneralName (RFC5280)
-
should_verify_common_name = false
-
return true if verify_hostname(hostname, san.value)
-
when 7 # iPAddress in GeneralName (RFC5280)
-
should_verify_common_name = false
-
if san.value.size == 4 || san.value.size == 16
-
begin
-
return true if san.value == IPAddr.new(hostname).hton
-
rescue IPAddr::InvalidAddressError
-
end
-
end
-
end
-
}
-
}
-
if should_verify_common_name
-
cert.subject.to_a.each{|oid, value|
-
if oid == "CN"
-
return true if verify_hostname(hostname, value)
-
end
-
}
-
end
-
return false
-
end
-
1
module_function :verify_certificate_identity
-
-
1
def verify_hostname(hostname, san) # :nodoc:
-
# RFC 5280, IA5String is limited to the set of ASCII characters
-
return false unless san.ascii_only?
-
return false unless hostname.ascii_only?
-
-
# See RFC 6125, section 6.4.1
-
# Matching is case-insensitive.
-
san_parts = san.downcase.split(".")
-
-
# TODO: this behavior should probably be more strict
-
return san == hostname if san_parts.size < 2
-
-
# Matching is case-insensitive.
-
host_parts = hostname.downcase.split(".")
-
-
# RFC 6125, section 6.4.3, subitem 2.
-
# If the wildcard character is the only character of the left-most
-
# label in the presented identifier, the client SHOULD NOT compare
-
# against anything but the left-most label of the reference
-
# identifier (e.g., *.example.com would match foo.example.com but
-
# not bar.foo.example.com or example.com).
-
return false unless san_parts.size == host_parts.size
-
-
# RFC 6125, section 6.4.3, subitem 1.
-
# The client SHOULD NOT attempt to match a presented identifier in
-
# which the wildcard character comprises a label other than the
-
# left-most label (e.g., do not match bar.*.example.net).
-
return false unless verify_wildcard(host_parts.shift, san_parts.shift)
-
-
san_parts.join(".") == host_parts.join(".")
-
end
-
1
module_function :verify_hostname
-
-
1
def verify_wildcard(domain_component, san_component) # :nodoc:
-
parts = san_component.split("*", -1)
-
-
return false if parts.size > 2
-
return san_component == domain_component if parts.size == 1
-
-
# RFC 6125, section 6.4.3, subitem 3.
-
# The client SHOULD NOT attempt to match a presented identifier
-
# where the wildcard character is embedded within an A-label or
-
# U-label of an internationalized domain name.
-
return false if domain_component.start_with?("xn--") && san_component != "*"
-
-
parts[0].length + parts[1].length < domain_component.length &&
-
domain_component.start_with?(parts[0]) &&
-
domain_component.end_with?(parts[1])
-
end
-
1
module_function :verify_wildcard
-
-
1
class SSLSocket
-
1
include Buffering
-
1
include SocketForwarder
-
-
1
attr_reader :hostname
-
-
# The underlying IO object.
-
1
attr_reader :io
-
1
alias :to_io :io
-
-
# The SSLContext object used in this connection.
-
1
attr_reader :context
-
-
# Whether to close the underlying socket as well, when the SSL/TLS
-
# connection is shut down. This defaults to +false+.
-
1
attr_accessor :sync_close
-
-
# call-seq:
-
# ssl.sysclose => nil
-
#
-
# Sends "close notify" to the peer and tries to shut down the SSL
-
# connection gracefully.
-
#
-
# If sync_close is set to +true+, the underlying IO is also closed.
-
1
def sysclose
-
return if closed?
-
stop
-
io.close if sync_close
-
end
-
-
# call-seq:
-
# ssl.post_connection_check(hostname) -> true
-
#
-
# Perform hostname verification following RFC 6125.
-
#
-
# This method MUST be called after calling #connect to ensure that the
-
# hostname of a remote peer has been verified.
-
1
def post_connection_check(hostname)
-
if peer_cert.nil?
-
msg = "Peer verification enabled, but no certificate received."
-
if using_anon_cipher?
-
msg += " Anonymous cipher suite #{cipher[0]} was negotiated. " \
-
"Anonymous suites must be disabled to use peer verification."
-
end
-
raise SSLError, msg
-
end
-
-
unless OpenSSL::SSL.verify_certificate_identity(peer_cert, hostname)
-
raise SSLError, "hostname \"#{hostname}\" does not match the server certificate"
-
end
-
return true
-
end
-
-
# call-seq:
-
# ssl.session -> aSession
-
#
-
# Returns the SSLSession object currently used, or nil if the session is
-
# not established.
-
1
def session
-
SSL::Session.new(self)
-
rescue SSL::Session::SessionError
-
nil
-
end
-
-
1
private
-
-
1
def using_anon_cipher?
-
ctx = OpenSSL::SSL::SSLContext.new
-
ctx.ciphers = "aNULL"
-
ctx.ciphers.include?(cipher)
-
end
-
-
1
def client_cert_cb
-
@context.client_cert_cb
-
end
-
-
1
def tmp_dh_callback
-
@context.tmp_dh_callback || OpenSSL::SSL::SSLContext::DEFAULT_TMP_DH_CALLBACK
-
end
-
-
1
def tmp_ecdh_callback
-
@context.tmp_ecdh_callback
-
end
-
-
1
def session_new_cb
-
@context.session_new_cb
-
end
-
-
1
def session_get_cb
-
@context.session_get_cb
-
end
-
end
-
-
##
-
# SSLServer represents a TCP/IP server socket with Secure Sockets Layer.
-
1
class SSLServer
-
1
include SocketForwarder
-
# When true then #accept works exactly the same as TCPServer#accept
-
1
attr_accessor :start_immediately
-
-
# Creates a new instance of SSLServer.
-
# * _srv_ is an instance of TCPServer.
-
# * _ctx_ is an instance of OpenSSL::SSL::SSLContext.
-
1
def initialize(svr, ctx)
-
@svr = svr
-
@ctx = ctx
-
unless ctx.session_id_context
-
# see #6137 - session id may not exceed 32 bytes
-
prng = ::Random.new($0.hash)
-
session_id = prng.bytes(16).unpack('H*')[0]
-
@ctx.session_id_context = session_id
-
end
-
@start_immediately = true
-
end
-
-
# Returns the TCPServer passed to the SSLServer when initialized.
-
1
def to_io
-
@svr
-
end
-
-
# See TCPServer#listen for details.
-
1
def listen(backlog=5)
-
@svr.listen(backlog)
-
end
-
-
# See BasicSocket#shutdown for details.
-
1
def shutdown(how=Socket::SHUT_RDWR)
-
@svr.shutdown(how)
-
end
-
-
# Works similar to TCPServer#accept.
-
1
def accept
-
# Socket#accept returns [socket, addrinfo].
-
# TCPServer#accept returns a socket.
-
# The following comma strips addrinfo.
-
sock, = @svr.accept
-
begin
-
ssl = OpenSSL::SSL::SSLSocket.new(sock, @ctx)
-
ssl.sync_close = true
-
ssl.accept if @start_immediately
-
ssl
-
rescue Exception => ex
-
if ssl
-
ssl.close
-
else
-
sock.close
-
end
-
raise ex
-
end
-
end
-
-
# See IO#close for details.
-
1
def close
-
@svr.close
-
end
-
end
-
end
-
end
-
# frozen_string_literal: false
-
#--
-
# = Ruby-space definitions that completes C-space funcs for X509 and subclasses
-
#
-
# = Info
-
# 'OpenSSL for Ruby 2' project
-
# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz>
-
# All rights reserved.
-
#
-
# = Licence
-
# This program is licensed under the same licence as Ruby.
-
# (See the file 'LICENCE'.)
-
#++
-
-
1
module OpenSSL
-
1
module X509
-
1
class ExtensionFactory
-
1
def create_extension(*arg)
-
if arg.size > 1
-
create_ext(*arg)
-
else
-
send("create_ext_from_"+arg[0].class.name.downcase, arg[0])
-
end
-
end
-
-
1
def create_ext_from_array(ary)
-
raise ExtensionError, "unexpected array form" if ary.size > 3
-
create_ext(ary[0], ary[1], ary[2])
-
end
-
-
1
def create_ext_from_string(str) # "oid = critical, value"
-
oid, value = str.split(/=/, 2)
-
oid.strip!
-
value.strip!
-
create_ext(oid, value)
-
end
-
-
1
def create_ext_from_hash(hash)
-
create_ext(hash["oid"], hash["value"], hash["critical"])
-
end
-
end
-
-
1
class Extension
-
1
def ==(other)
-
return false unless Extension === other
-
to_der == other.to_der
-
end
-
-
1
def to_s # "oid = critical, value"
-
str = self.oid
-
str << " = "
-
str << "critical, " if self.critical?
-
str << self.value.gsub(/\n/, ", ")
-
end
-
-
1
def to_h # {"oid"=>sn|ln, "value"=>value, "critical"=>true|false}
-
{"oid"=>self.oid,"value"=>self.value,"critical"=>self.critical?}
-
end
-
-
1
def to_a
-
[ self.oid, self.value, self.critical? ]
-
end
-
end
-
-
1
class Name
-
1
module RFC2253DN
-
1
Special = ',=+<>#;'
-
1
HexChar = /[0-9a-fA-F]/
-
1
HexPair = /#{HexChar}#{HexChar}/
-
1
HexString = /#{HexPair}+/
-
1
Pair = /\\(?:[#{Special}]|\\|"|#{HexPair})/
-
1
StringChar = /[^\\"#{Special}]/
-
1
QuoteChar = /[^\\"]/
-
1
AttributeType = /[a-zA-Z][0-9a-zA-Z]*|[0-9]+(?:\.[0-9]+)*/
-
1
AttributeValue = /
-
(?!["#])((?:#{StringChar}|#{Pair})*)|
-
\#(#{HexString})|
-
"((?:#{QuoteChar}|#{Pair})*)"
-
/x
-
1
TypeAndValue = /\A(#{AttributeType})=#{AttributeValue}/
-
-
1
module_function
-
-
1
def expand_pair(str)
-
return nil unless str
-
return str.gsub(Pair){
-
pair = $&
-
case pair.size
-
when 2 then pair[1,1]
-
when 3 then Integer("0x#{pair[1,2]}").chr
-
else raise OpenSSL::X509::NameError, "invalid pair: #{str}"
-
end
-
}
-
end
-
-
1
def expand_hexstring(str)
-
return nil unless str
-
der = str.gsub(HexPair){$&.to_i(16).chr }
-
a1 = OpenSSL::ASN1.decode(der)
-
return a1.value, a1.tag
-
end
-
-
1
def expand_value(str1, str2, str3)
-
value = expand_pair(str1)
-
value, tag = expand_hexstring(str2) unless value
-
value = expand_pair(str3) unless value
-
return value, tag
-
end
-
-
1
def scan(dn)
-
str = dn
-
ary = []
-
while true
-
if md = TypeAndValue.match(str)
-
remain = md.post_match
-
type = md[1]
-
value, tag = expand_value(md[2], md[3], md[4]) rescue nil
-
if value
-
type_and_value = [type, value]
-
type_and_value.push(tag) if tag
-
ary.unshift(type_and_value)
-
if remain.length > 2 && remain[0] == ?,
-
str = remain[1..-1]
-
next
-
elsif remain.length > 2 && remain[0] == ?+
-
raise OpenSSL::X509::NameError,
-
"multi-valued RDN is not supported: #{dn}"
-
elsif remain.empty?
-
break
-
end
-
end
-
end
-
msg_dn = dn[0, dn.length - str.length] + " =>" + str
-
raise OpenSSL::X509::NameError, "malformed RDN: #{msg_dn}"
-
end
-
return ary
-
end
-
end
-
-
1
class << self
-
1
def parse_rfc2253(str, template=OBJECT_TYPE_TEMPLATE)
-
ary = OpenSSL::X509::Name::RFC2253DN.scan(str)
-
self.new(ary, template)
-
end
-
-
1
def parse_openssl(str, template=OBJECT_TYPE_TEMPLATE)
-
if str.start_with?("/")
-
# /A=B/C=D format
-
ary = str[1..-1].split("/").map { |i| i.split("=", 2) }
-
else
-
# Comma-separated
-
ary = str.split(",").map { |i| i.strip.split("=", 2) }
-
end
-
self.new(ary, template)
-
end
-
-
1
alias parse parse_openssl
-
end
-
-
1
def pretty_print(q)
-
q.object_group(self) {
-
q.text ' '
-
q.text to_s(OpenSSL::X509::Name::RFC2253)
-
}
-
end
-
end
-
-
1
class Attribute
-
1
def ==(other)
-
return false unless Attribute === other
-
to_der == other.to_der
-
end
-
end
-
-
1
class StoreContext
-
1
def cleanup
-
warn "(#{caller.first}) OpenSSL::X509::StoreContext#cleanup is deprecated with no replacement" if $VERBOSE
-
end
-
end
-
-
1
class Certificate
-
1
def pretty_print(q)
-
q.object_group(self) {
-
q.breakable
-
q.text 'subject='; q.pp self.subject; q.text ','; q.breakable
-
q.text 'issuer='; q.pp self.issuer; q.text ','; q.breakable
-
q.text 'serial='; q.pp self.serial; q.text ','; q.breakable
-
q.text 'not_before='; q.pp self.not_before; q.text ','; q.breakable
-
q.text 'not_after='; q.pp self.not_after
-
}
-
end
-
end
-
-
1
class CRL
-
1
def ==(other)
-
return false unless CRL === other
-
to_der == other.to_der
-
end
-
end
-
-
1
class Revoked
-
1
def ==(other)
-
return false unless Revoked === other
-
to_der == other.to_der
-
end
-
end
-
-
1
class Request
-
1
def ==(other)
-
return false unless Request === other
-
to_der == other.to_der
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
#
-
# optparse.rb - command-line option analysis with the OptionParser class.
-
#
-
# Author:: Nobu Nakada
-
# Documentation:: Nobu Nakada and Gavin Sinclair.
-
#
-
# See OptionParser for documentation.
-
#
-
-
-
#--
-
# == Developer Documentation (not for RDoc output)
-
#
-
# === Class tree
-
#
-
# - OptionParser:: front end
-
# - OptionParser::Switch:: each switches
-
# - OptionParser::List:: options list
-
# - OptionParser::ParseError:: errors on parsing
-
# - OptionParser::AmbiguousOption
-
# - OptionParser::NeedlessArgument
-
# - OptionParser::MissingArgument
-
# - OptionParser::InvalidOption
-
# - OptionParser::InvalidArgument
-
# - OptionParser::AmbiguousArgument
-
#
-
# === Object relationship diagram
-
#
-
# +--------------+
-
# | OptionParser |<>-----+
-
# +--------------+ | +--------+
-
# | ,-| Switch |
-
# on_head -------->+---------------+ / +--------+
-
# accept/reject -->| List |<|>-
-
# | |<|>- +----------+
-
# on ------------->+---------------+ `-| argument |
-
# : : | class |
-
# +---------------+ |==========|
-
# on_tail -------->| | |pattern |
-
# +---------------+ |----------|
-
# OptionParser.accept ->| DefaultList | |converter |
-
# reject |(shared between| +----------+
-
# | all instances)|
-
# +---------------+
-
#
-
#++
-
#
-
# == OptionParser
-
#
-
# === Introduction
-
#
-
# OptionParser is a class for command-line option analysis. It is much more
-
# advanced, yet also easier to use, than GetoptLong, and is a more Ruby-oriented
-
# solution.
-
#
-
# === Features
-
#
-
# 1. The argument specification and the code to handle it are written in the
-
# same place.
-
# 2. It can output an option summary; you don't need to maintain this string
-
# separately.
-
# 3. Optional and mandatory arguments are specified very gracefully.
-
# 4. Arguments can be automatically converted to a specified class.
-
# 5. Arguments can be restricted to a certain set.
-
#
-
# All of these features are demonstrated in the examples below. See
-
# #make_switch for full documentation.
-
#
-
# === Minimal example
-
#
-
# require 'optparse'
-
#
-
# options = {}
-
# OptionParser.new do |opts|
-
# opts.banner = "Usage: example.rb [options]"
-
#
-
# opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
-
# options[:verbose] = v
-
# end
-
# end.parse!
-
#
-
# p options
-
# p ARGV
-
#
-
# === Generating Help
-
#
-
# OptionParser can be used to automatically generate help for the commands you
-
# write:
-
#
-
# require 'optparse'
-
#
-
# Options = Struct.new(:name)
-
#
-
# class Parser
-
# def self.parse(options)
-
# args = Options.new("world")
-
#
-
# opt_parser = OptionParser.new do |opts|
-
# opts.banner = "Usage: example.rb [options]"
-
#
-
# opts.on("-nNAME", "--name=NAME", "Name to say hello to") do |n|
-
# args.name = n
-
# end
-
#
-
# opts.on("-h", "--help", "Prints this help") do
-
# puts opts
-
# exit
-
# end
-
# end
-
#
-
# opt_parser.parse!(options)
-
# return args
-
# end
-
# end
-
# options = Parser.parse %w[--help]
-
#
-
# #=>
-
# # Usage: example.rb [options]
-
# # -n, --name=NAME Name to say hello to
-
# # -h, --help Prints this help
-
#
-
# === Required Arguments
-
#
-
# For options that require an argument, option specification strings may include an
-
# option name in all caps. If an option is used without the required argument,
-
# an exception will be raised.
-
#
-
# require 'optparse'
-
#
-
# options = {}
-
# OptionParser.new do |parser|
-
# parser.on("-r", "--require LIBRARY",
-
# "Require the LIBRARY before executing your script") do |lib|
-
# puts "You required #{lib}!"
-
# end
-
# end.parse!
-
#
-
# Used:
-
#
-
# $ ruby optparse-test.rb -r
-
# optparse-test.rb:9:in `<main>': missing argument: -r (OptionParser::MissingArgument)
-
# $ ruby optparse-test.rb -r my-library
-
# You required my-library!
-
#
-
# === Type Coercion
-
#
-
# OptionParser supports the ability to coerce command line arguments
-
# into objects for us.
-
#
-
# OptionParser comes with a few ready-to-use kinds of type
-
# coercion. They are:
-
#
-
# - Date -- Anything accepted by +Date.parse+
-
# - DateTime -- Anything accepted by +DateTime.parse+
-
# - Time -- Anything accepted by +Time.httpdate+ or +Time.parse+
-
# - URI -- Anything accepted by +URI.parse+
-
# - Shellwords -- Anything accepted by +Shellwords.shellwords+
-
# - String -- Any non-empty string
-
# - Integer -- Any integer. Will convert octal. (e.g. 124, -3, 040)
-
# - Float -- Any float. (e.g. 10, 3.14, -100E+13)
-
# - Numeric -- Any integer, float, or rational (1, 3.4, 1/3)
-
# - DecimalInteger -- Like +Integer+, but no octal format.
-
# - OctalInteger -- Like +Integer+, but no decimal format.
-
# - DecimalNumeric -- Decimal integer or float.
-
# - TrueClass -- Accepts '+, yes, true, -, no, false' and
-
# defaults as +true+
-
# - FalseClass -- Same as +TrueClass+, but defaults to +false+
-
# - Array -- Strings separated by ',' (e.g. 1,2,3)
-
# - Regexp -- Regular expressions. Also includes options.
-
#
-
# We can also add our own coercions, which we will cover below.
-
#
-
# ==== Using Built-in Conversions
-
#
-
# As an example, the built-in +Time+ conversion is used. The other built-in
-
# conversions behave in the same way.
-
# OptionParser will attempt to parse the argument
-
# as a +Time+. If it succeeds, that time will be passed to the
-
# handler block. Otherwise, an exception will be raised.
-
#
-
# require 'optparse'
-
# require 'optparse/time'
-
# OptionParser.new do |parser|
-
# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
-
# p time
-
# end
-
# end.parse!
-
#
-
# Used:
-
#
-
# $ ruby optparse-test.rb -t nonsense
-
# ... invalid argument: -t nonsense (OptionParser::InvalidArgument)
-
# $ ruby optparse-test.rb -t 10-11-12
-
# 2010-11-12 00:00:00 -0500
-
# $ ruby optparse-test.rb -t 9:30
-
# 2014-08-13 09:30:00 -0400
-
#
-
# ==== Creating Custom Conversions
-
#
-
# The +accept+ method on OptionParser may be used to create converters.
-
# It specifies which conversion block to call whenever a class is specified.
-
# The example below uses it to fetch a +User+ object before the +on+ handler receives it.
-
#
-
# require 'optparse'
-
#
-
# User = Struct.new(:id, :name)
-
#
-
# def find_user id
-
# not_found = ->{ raise "No User Found for id #{id}" }
-
# [ User.new(1, "Sam"),
-
# User.new(2, "Gandalf") ].find(not_found) do |u|
-
# u.id == id
-
# end
-
# end
-
#
-
# op = OptionParser.new
-
# op.accept(User) do |user_id|
-
# find_user user_id.to_i
-
# end
-
#
-
# op.on("--user ID", User) do |user|
-
# puts user
-
# end
-
#
-
# op.parse!
-
#
-
# Used:
-
#
-
# $ ruby optparse-test.rb --user 1
-
# #<struct User id=1, name="Sam">
-
# $ ruby optparse-test.rb --user 2
-
# #<struct User id=2, name="Gandalf">
-
# $ ruby optparse-test.rb --user 3
-
# optparse-test.rb:15:in `block in find_user': No User Found for id 3 (RuntimeError)
-
#
-
# === Store options to a Hash
-
#
-
# The +into+ option of +order+, +parse+ and so on methods stores command line options into a Hash.
-
#
-
# require 'optparse'
-
#
-
# params = {}
-
# OptionParser.new do |opts|
-
# opts.on('-a')
-
# opts.on('-b NUM', Integer)
-
# opts.on('-v', '--verbose')
-
# end.parse!(into: params)
-
#
-
# p params
-
#
-
# Used:
-
#
-
# $ ruby optparse-test.rb -a
-
# {:a=>true}
-
# $ ruby optparse-test.rb -a -v
-
# {:a=>true, :verbose=>true}
-
# $ ruby optparse-test.rb -a -b 100
-
# {:a=>true, :b=>100}
-
#
-
# === Complete example
-
#
-
# The following example is a complete Ruby program. You can run it and see the
-
# effect of specifying various options. This is probably the best way to learn
-
# the features of +optparse+.
-
#
-
# require 'optparse'
-
# require 'optparse/time'
-
# require 'ostruct'
-
# require 'pp'
-
#
-
# class OptparseExample
-
# Version = '1.0.0'
-
#
-
# CODES = %w[iso-2022-jp shift_jis euc-jp utf8 binary]
-
# CODE_ALIASES = { "jis" => "iso-2022-jp", "sjis" => "shift_jis" }
-
#
-
# class ScriptOptions
-
# attr_accessor :library, :inplace, :encoding, :transfer_type,
-
# :verbose, :extension, :delay, :time, :record_separator,
-
# :list
-
#
-
# def initialize
-
# self.library = []
-
# self.inplace = false
-
# self.encoding = "utf8"
-
# self.transfer_type = :auto
-
# self.verbose = false
-
# end
-
#
-
# def define_options(parser)
-
# parser.banner = "Usage: example.rb [options]"
-
# parser.separator ""
-
# parser.separator "Specific options:"
-
#
-
# # add additional options
-
# perform_inplace_option(parser)
-
# delay_execution_option(parser)
-
# execute_at_time_option(parser)
-
# specify_record_separator_option(parser)
-
# list_example_option(parser)
-
# specify_encoding_option(parser)
-
# optional_option_argument_with_keyword_completion_option(parser)
-
# boolean_verbose_option(parser)
-
#
-
# parser.separator ""
-
# parser.separator "Common options:"
-
# # No argument, shows at tail. This will print an options summary.
-
# # Try it and see!
-
# parser.on_tail("-h", "--help", "Show this message") do
-
# puts parser
-
# exit
-
# end
-
# # Another typical switch to print the version.
-
# parser.on_tail("--version", "Show version") do
-
# puts Version
-
# exit
-
# end
-
# end
-
#
-
# def perform_inplace_option(parser)
-
# # Specifies an optional option argument
-
# parser.on("-i", "--inplace [EXTENSION]",
-
# "Edit ARGV files in place",
-
# "(make backup if EXTENSION supplied)") do |ext|
-
# self.inplace = true
-
# self.extension = ext || ''
-
# self.extension.sub!(/\A\.?(?=.)/, ".") # Ensure extension begins with dot.
-
# end
-
# end
-
#
-
# def delay_execution_option(parser)
-
# # Cast 'delay' argument to a Float.
-
# parser.on("--delay N", Float, "Delay N seconds before executing") do |n|
-
# self.delay = n
-
# end
-
# end
-
#
-
# def execute_at_time_option(parser)
-
# # Cast 'time' argument to a Time object.
-
# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
-
# self.time = time
-
# end
-
# end
-
#
-
# def specify_record_separator_option(parser)
-
# # Cast to octal integer.
-
# parser.on("-F", "--irs [OCTAL]", OptionParser::OctalInteger,
-
# "Specify record separator (default \\0)") do |rs|
-
# self.record_separator = rs
-
# end
-
# end
-
#
-
# def list_example_option(parser)
-
# # List of arguments.
-
# parser.on("--list x,y,z", Array, "Example 'list' of arguments") do |list|
-
# self.list = list
-
# end
-
# end
-
#
-
# def specify_encoding_option(parser)
-
# # Keyword completion. We are specifying a specific set of arguments (CODES
-
# # and CODE_ALIASES - notice the latter is a Hash), and the user may provide
-
# # the shortest unambiguous text.
-
# code_list = (CODE_ALIASES.keys + CODES).join(', ')
-
# parser.on("--code CODE", CODES, CODE_ALIASES, "Select encoding",
-
# "(#{code_list})") do |encoding|
-
# self.encoding = encoding
-
# end
-
# end
-
#
-
# def optional_option_argument_with_keyword_completion_option(parser)
-
# # Optional '--type' option argument with keyword completion.
-
# parser.on("--type [TYPE]", [:text, :binary, :auto],
-
# "Select transfer type (text, binary, auto)") do |t|
-
# self.transfer_type = t
-
# end
-
# end
-
#
-
# def boolean_verbose_option(parser)
-
# # Boolean switch.
-
# parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
-
# self.verbose = v
-
# end
-
# end
-
# end
-
#
-
# #
-
# # Return a structure describing the options.
-
# #
-
# def parse(args)
-
# # The options specified on the command line will be collected in
-
# # *options*.
-
#
-
# @options = ScriptOptions.new
-
# @args = OptionParser.new do |parser|
-
# @options.define_options(parser)
-
# parser.parse!(args)
-
# end
-
# @options
-
# end
-
#
-
# attr_reader :parser, :options
-
# end # class OptparseExample
-
#
-
# example = OptparseExample.new
-
# options = example.parse(ARGV)
-
# pp options # example.options
-
# pp ARGV
-
#
-
# === Shell Completion
-
#
-
# For modern shells (e.g. bash, zsh, etc.), you can use shell
-
# completion for command line options.
-
#
-
# === Further documentation
-
#
-
# The above examples should be enough to learn how to use this class. If you
-
# have any questions, file a ticket at http://bugs.ruby-lang.org.
-
#
-
1
class OptionParser
-
# :stopdoc:
-
1
NoArgument = [NO_ARGUMENT = :NONE, nil].freeze
-
1
RequiredArgument = [REQUIRED_ARGUMENT = :REQUIRED, true].freeze
-
1
OptionalArgument = [OPTIONAL_ARGUMENT = :OPTIONAL, false].freeze
-
# :startdoc:
-
-
#
-
# Keyword completion module. This allows partial arguments to be specified
-
# and resolved against a list of acceptable values.
-
#
-
1
module Completion
-
1
def self.regexp(key, icase)
-
Regexp.new('\A' + Regexp.quote(key).gsub(/\w+\b/, '\&\w*'), icase)
-
end
-
-
1
def self.candidate(key, icase = false, pat = nil, &block)
-
pat ||= Completion.regexp(key, icase)
-
candidates = []
-
block.call do |k, *v|
-
(if Regexp === k
-
kn = ""
-
k === key
-
else
-
kn = defined?(k.id2name) ? k.id2name : k
-
pat === kn
-
end) or next
-
v << k if v.empty?
-
candidates << [k, v, kn]
-
end
-
candidates
-
end
-
-
1
def candidate(key, icase = false, pat = nil)
-
Completion.candidate(key, icase, pat, &method(:each))
-
end
-
-
1
public
-
1
def complete(key, icase = false, pat = nil)
-
candidates = candidate(key, icase, pat, &method(:each)).sort_by {|k, v, kn| kn.size}
-
if candidates.size == 1
-
canon, sw, * = candidates[0]
-
elsif candidates.size > 1
-
canon, sw, cn = candidates.shift
-
candidates.each do |k, v, kn|
-
next if sw == v
-
if String === cn and String === kn
-
if cn.rindex(kn, 0)
-
canon, sw, cn = k, v, kn
-
next
-
elsif kn.rindex(cn, 0)
-
next
-
end
-
end
-
throw :ambiguous, key
-
end
-
end
-
if canon
-
block_given? or return key, *sw
-
yield(key, *sw)
-
end
-
end
-
-
1
def convert(opt = nil, val = nil, *)
-
val
-
end
-
end
-
-
-
#
-
# Map from option/keyword string to object with completion.
-
#
-
1
class OptionMap < Hash
-
1
include Completion
-
end
-
-
-
#
-
# Individual switch class. Not important to the user.
-
#
-
# Defined within Switch are several Switch-derived classes: NoArgument,
-
# RequiredArgument, etc.
-
#
-
1
class Switch
-
1
attr_reader :pattern, :conv, :short, :long, :arg, :desc, :block
-
-
#
-
# Guesses argument style from +arg+. Returns corresponding
-
# OptionParser::Switch class (OptionalArgument, etc.).
-
#
-
1
def self.guess(arg)
-
3
case arg
-
when ""
-
t = self
-
when /\A=?\[/
-
t = Switch::OptionalArgument
-
when /\A\s+\[/
-
t = Switch::PlacedArgument
-
else
-
3
t = Switch::RequiredArgument
-
end
-
3
self >= t or incompatible_argument_styles(arg, t)
-
3
t
-
end
-
-
1
def self.incompatible_argument_styles(arg, t)
-
raise(ArgumentError, "#{arg}: incompatible argument styles\n #{self}, #{t}",
-
ParseError.filter_backtrace(caller(2)))
-
end
-
-
1
def self.pattern
-
NilClass
-
end
-
-
1
def initialize(pattern = nil, conv = nil,
-
short = nil, long = nil, arg = nil,
-
6
desc = ([] if short or long), block = nil, &_block)
-
13
raise if Array === pattern
-
13
block ||= _block
-
@pattern, @conv, @short, @long, @arg, @desc, @block =
-
13
pattern, conv, short, long, arg, desc, block
-
end
-
-
#
-
# Parses +arg+ and returns rest of +arg+ and matched portion to the
-
# argument pattern. Yields when the pattern doesn't match substring.
-
#
-
1
def parse_arg(arg)
-
pattern or return nil, [arg]
-
unless m = pattern.match(arg)
-
yield(InvalidArgument, arg)
-
return arg, []
-
end
-
if String === m
-
m = [s = m]
-
else
-
m = m.to_a
-
s = m[0]
-
return nil, m unless String === s
-
end
-
raise InvalidArgument, arg unless arg.rindex(s, 0)
-
return nil, m if s.length == arg.length
-
yield(InvalidArgument, arg) # didn't match whole arg
-
return arg[s.length..-1], m
-
end
-
1
private :parse_arg
-
-
#
-
# Parses argument, converts and returns +arg+, +block+ and result of
-
# conversion. Yields at semi-error condition instead of raising an
-
# exception.
-
#
-
1
def conv_arg(arg, val = [])
-
if conv
-
val = conv.call(*val)
-
else
-
val = proc {|v| v}.call(*val)
-
end
-
return arg, block, val
-
end
-
1
private :conv_arg
-
-
#
-
# Produces the summary text. Each line of the summary is yielded to the
-
# block (without newline).
-
#
-
# +sdone+:: Already summarized short style options keyed hash.
-
# +ldone+:: Already summarized long style options keyed hash.
-
# +width+:: Width of left side (option part). In other words, the right
-
# side (description part) starts after +width+ columns.
-
# +max+:: Maximum width of left side -> the options are filled within
-
# +max+ columns.
-
# +indent+:: Prefix string indents all summarized lines.
-
#
-
1
def summarize(sdone = {}, ldone = {}, width = 1, max = width - 1, indent = "")
-
sopts, lopts = [], [], nil
-
@short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short
-
@long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long
-
return if sopts.empty? and lopts.empty? # completely hidden
-
-
left = [sopts.join(', ')]
-
right = desc.dup
-
-
while s = lopts.shift
-
l = left[-1].length + s.length
-
l += arg.length if left.size == 1 && arg
-
l < max or sopts.empty? or left << +''
-
left[-1] << (left[-1].empty? ? ' ' * 4 : ', ') << s
-
end
-
-
if arg
-
left[0] << (left[1] ? arg.sub(/\A(\[?)=/, '\1') + ',' : arg)
-
end
-
mlen = left.collect {|ss| ss.length}.max.to_i
-
while mlen > width and l = left.shift
-
mlen = left.collect {|ss| ss.length}.max.to_i if l.length == mlen
-
if l.length < width and (r = right[0]) and !r.empty?
-
l = l.to_s.ljust(width) + ' ' + r
-
right.shift
-
end
-
yield(indent + l)
-
end
-
-
while begin l = left.shift; r = right.shift; l or r end
-
l = l.to_s.ljust(width) + ' ' + r if r and !r.empty?
-
yield(indent + l)
-
end
-
-
self
-
end
-
-
1
def add_banner(to) # :nodoc:
-
unless @short or @long
-
s = desc.join
-
to << " [" + s + "]..." unless s.empty?
-
end
-
to
-
end
-
-
1
def match_nonswitch?(str) # :nodoc:
-
@pattern =~ str unless @short or @long
-
end
-
-
#
-
# Main name of the switch.
-
#
-
1
def switch_name
-
(long.first || short.first).sub(/\A-+(?:\[no-\])?/, '')
-
end
-
-
1
def compsys(sdone, ldone) # :nodoc:
-
sopts, lopts = [], []
-
@short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short
-
@long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long
-
return if sopts.empty? and lopts.empty? # completely hidden
-
-
(sopts+lopts).each do |opt|
-
# "(-x -c -r)-l[left justify]"
-
if /^--\[no-\](.+)$/ =~ opt
-
o = $1
-
yield("--#{o}", desc.join(""))
-
yield("--no-#{o}", desc.join(""))
-
else
-
yield("#{opt}", desc.join(""))
-
end
-
end
-
end
-
-
#
-
# Switch that takes no arguments.
-
#
-
1
class NoArgument < self
-
-
#
-
# Raises an exception if any arguments given.
-
#
-
1
def parse(arg, argv)
-
yield(NeedlessArgument, arg) if arg
-
conv_arg(arg)
-
end
-
-
1
def self.incompatible_argument_styles(*)
-
end
-
-
1
def self.pattern
-
3
Object
-
end
-
end
-
-
#
-
# Switch that takes an argument.
-
#
-
1
class RequiredArgument < self
-
-
#
-
# Raises an exception if argument is not present.
-
#
-
1
def parse(arg, argv)
-
unless arg
-
raise MissingArgument if argv.empty?
-
arg = argv.shift
-
end
-
conv_arg(*parse_arg(arg, &method(:raise)))
-
end
-
end
-
-
#
-
# Switch that can omit argument.
-
#
-
1
class OptionalArgument < self
-
-
#
-
# Parses argument if given, or uses default value.
-
#
-
1
def parse(arg, argv, &error)
-
if arg
-
conv_arg(*parse_arg(arg, &error))
-
else
-
conv_arg(arg)
-
end
-
end
-
end
-
-
#
-
# Switch that takes an argument, which does not begin with '-'.
-
#
-
1
class PlacedArgument < self
-
-
#
-
# Returns nil if argument is not present or begins with '-'.
-
#
-
1
def parse(arg, argv, &error)
-
if !(val = arg) and (argv.empty? or /\A-/ =~ (val = argv[0]))
-
return nil, block, nil
-
end
-
opt = (val = parse_arg(val, &error))[1]
-
val = conv_arg(*val)
-
if opt and !arg
-
argv.shift
-
else
-
val[0] = nil
-
end
-
val
-
end
-
end
-
end
-
-
#
-
# Simple option list providing mapping from short and/or long option
-
# string to OptionParser::Switch and mapping from acceptable argument to
-
# matching pattern and converter pair. Also provides summary feature.
-
#
-
1
class List
-
# Map from acceptable argument types to pattern and converter pairs.
-
1
attr_reader :atype
-
-
# Map from short style option switches to actual switch objects.
-
1
attr_reader :short
-
-
# Map from long style option switches to actual switch objects.
-
1
attr_reader :long
-
-
# List of all switches and summary string.
-
1
attr_reader :list
-
-
#
-
# Just initializes all instance variables.
-
#
-
1
def initialize
-
3
@atype = {}
-
3
@short = OptionMap.new
-
3
@long = OptionMap.new
-
3
@list = []
-
end
-
-
#
-
# See OptionParser.accept.
-
#
-
1
def accept(t, pat = /.*/m, &block)
-
13
if pat
-
13
pat.respond_to?(:match) or
-
raise TypeError, "has no `match'", ParseError.filter_backtrace(caller(2))
-
else
-
pat = t if t.respond_to?(:match)
-
end
-
13
unless block
-
block = pat.method(:convert).to_proc if pat.respond_to?(:convert)
-
end
-
13
@atype[t] = [pat, block]
-
end
-
-
#
-
# See OptionParser.reject.
-
#
-
1
def reject(t)
-
@atype.delete(t)
-
end
-
-
#
-
# Adds +sw+ according to +sopts+, +lopts+ and +nlopts+.
-
#
-
# +sw+:: OptionParser::Switch instance to be added.
-
# +sopts+:: Short style option list.
-
# +lopts+:: Long style option list.
-
# +nlopts+:: Negated long style options list.
-
#
-
1
def update(sw, sopts, lopts, nsw = nil, nlopts = nil)
-
15
sopts.each {|o| @short[o] = sw} if sopts
-
16
lopts.each {|o| @long[o] = sw} if lopts
-
9
nlopts.each {|o| @long[o] = nsw} if nsw and nlopts
-
9
used = @short.invert.update(@long.invert)
-
45
@list.delete_if {|o| Switch === o and !used[o]}
-
end
-
1
private :update
-
-
#
-
# Inserts +switch+ at the head of the list, and associates short, long
-
# and negated long options. Arguments are:
-
#
-
# +switch+:: OptionParser::Switch instance to be inserted.
-
# +short_opts+:: List of short style options.
-
# +long_opts+:: List of long style options.
-
# +nolong_opts+:: List of long style options with "no-" prefix.
-
#
-
# prepend(switch, short_opts, long_opts, nolong_opts)
-
#
-
1
def prepend(*args)
-
update(*args)
-
@list.unshift(args[0])
-
end
-
-
#
-
# Appends +switch+ at the tail of the list, and associates short, long
-
# and negated long options. Arguments are:
-
#
-
# +switch+:: OptionParser::Switch instance to be inserted.
-
# +short_opts+:: List of short style options.
-
# +long_opts+:: List of long style options.
-
# +nolong_opts+:: List of long style options with "no-" prefix.
-
#
-
# append(switch, short_opts, long_opts, nolong_opts)
-
#
-
1
def append(*args)
-
9
update(*args)
-
9
@list.push(args[0])
-
end
-
-
#
-
# Searches +key+ in +id+ list. The result is returned or yielded if a
-
# block is given. If it isn't found, nil is returned.
-
#
-
1
def search(id, key)
-
90
if list = __send__(id)
-
171
val = list.fetch(key) {return nil}
-
9
block_given? ? yield(val) : val
-
end
-
end
-
-
#
-
# Searches list +id+ for +opt+ and the optional patterns for completion
-
# +pat+. If +icase+ is true, the search is case insensitive. The result
-
# is returned or yielded if a block is given. If it isn't found, nil is
-
# returned.
-
#
-
1
def complete(id, opt, icase = false, *pat, &block)
-
__send__(id).complete(opt, icase, *pat, &block)
-
end
-
-
1
def get_candidates(id)
-
yield __send__(id).keys
-
end
-
-
#
-
# Iterates over each option, passing the option to the +block+.
-
#
-
1
def each_option(&block)
-
list.each(&block)
-
end
-
-
#
-
# Creates the summary table, passing each line to the +block+ (without
-
# newline). The arguments +args+ are passed along to the summarize
-
# method which is called on every option.
-
#
-
1
def summarize(*args, &block)
-
sum = []
-
list.reverse_each do |opt|
-
if opt.respond_to?(:summarize) # perhaps OptionParser::Switch
-
s = []
-
opt.summarize(*args) {|l| s << l}
-
sum.concat(s.reverse)
-
elsif !opt or opt.empty?
-
sum << ""
-
elsif opt.respond_to?(:each_line)
-
sum.concat([*opt.each_line].reverse)
-
else
-
sum.concat([*opt.each].reverse)
-
end
-
end
-
sum.reverse_each(&block)
-
end
-
-
1
def add_banner(to) # :nodoc:
-
list.each do |opt|
-
if opt.respond_to?(:add_banner)
-
opt.add_banner(to)
-
end
-
end
-
to
-
end
-
-
1
def compsys(*args, &block) # :nodoc:
-
list.each do |opt|
-
if opt.respond_to?(:compsys)
-
opt.compsys(*args, &block)
-
end
-
end
-
end
-
end
-
-
#
-
# Hash with completion search feature. See OptionParser::Completion.
-
#
-
1
class CompletingHash < Hash
-
1
include Completion
-
-
#
-
# Completion for hash key.
-
#
-
1
def match(key)
-
*values = fetch(key) {
-
raise AmbiguousArgument, catch(:ambiguous) {return complete(key)}
-
}
-
return key, *values
-
end
-
end
-
-
# :stopdoc:
-
-
#
-
# Enumeration of acceptable argument styles. Possible values are:
-
#
-
# NO_ARGUMENT:: The switch takes no arguments. (:NONE)
-
# REQUIRED_ARGUMENT:: The switch requires an argument. (:REQUIRED)
-
# OPTIONAL_ARGUMENT:: The switch requires an optional argument. (:OPTIONAL)
-
#
-
# Use like --switch=argument (long style) or -Xargument (short style). For
-
# short style, only portion matched to argument pattern is treated as
-
# argument.
-
#
-
1
ArgumentStyle = {}
-
3
NoArgument.each {|el| ArgumentStyle[el] = Switch::NoArgument}
-
3
RequiredArgument.each {|el| ArgumentStyle[el] = Switch::RequiredArgument}
-
3
OptionalArgument.each {|el| ArgumentStyle[el] = Switch::OptionalArgument}
-
1
ArgumentStyle.freeze
-
-
#
-
# Switches common used such as '--', and also provides default
-
# argument classes
-
#
-
1
DefaultList = List.new
-
1
DefaultList.short['-'] = Switch::NoArgument.new {}
-
1
DefaultList.long[''] = Switch::NoArgument.new {throw :terminate}
-
-
-
1
COMPSYS_HEADER = <<'XXX' # :nodoc:
-
-
typeset -A opt_args
-
local context state line
-
-
_arguments -s -S \
-
XXX
-
-
1
def compsys(to, name = File.basename($0)) # :nodoc:
-
to << "#compdef #{name}\n"
-
to << COMPSYS_HEADER
-
visit(:compsys, {}, {}) {|o, d|
-
to << %Q[ "#{o}[#{d.gsub(/[\"\[\]]/, '\\\\\&')}]" \\\n]
-
}
-
to << " '*:file:_files' && return 0\n"
-
end
-
-
#
-
# Default options for ARGV, which never appear in option summary.
-
#
-
1
Officious = {}
-
-
#
-
# --help
-
# Shows option summary.
-
#
-
1
Officious['help'] = proc do |parser|
-
1
Switch::NoArgument.new do |arg|
-
puts parser.help
-
exit
-
end
-
end
-
-
#
-
# --*-completion-bash=WORD
-
# Shows candidates for command line completion.
-
#
-
1
Officious['*-completion-bash'] = proc do |parser|
-
1
Switch::RequiredArgument.new do |arg|
-
puts parser.candidate(arg)
-
exit
-
end
-
end
-
-
#
-
# --*-completion-zsh[=NAME:FILE]
-
# Creates zsh completion file.
-
#
-
1
Officious['*-completion-zsh'] = proc do |parser|
-
1
Switch::OptionalArgument.new do |arg|
-
parser.compsys(STDOUT, arg)
-
exit
-
end
-
end
-
-
#
-
# --version
-
# Shows version string if Version is defined.
-
#
-
1
Officious['version'] = proc do |parser|
-
1
Switch::OptionalArgument.new do |pkg|
-
if pkg
-
begin
-
require 'optparse/version'
-
rescue LoadError
-
else
-
show_version(*pkg.split(/,/)) or
-
abort("#{parser.program_name}: no version found in package #{pkg}")
-
exit
-
end
-
end
-
v = parser.ver or abort("#{parser.program_name}: version unknown")
-
puts v
-
exit
-
end
-
end
-
-
# :startdoc:
-
-
#
-
# Class methods
-
#
-
-
#
-
# Initializes a new instance and evaluates the optional block in context
-
# of the instance. Arguments +args+ are passed to #new, see there for
-
# description of parameters.
-
#
-
# This method is *deprecated*, its behavior corresponds to the older #new
-
# method.
-
#
-
1
def self.with(*args, &block)
-
opts = new(*args)
-
opts.instance_eval(&block)
-
opts
-
end
-
-
#
-
# Returns an incremented value of +default+ according to +arg+.
-
#
-
1
def self.inc(arg, default = nil)
-
case arg
-
when Integer
-
arg.nonzero?
-
when nil
-
default.to_i + 1
-
end
-
end
-
1
def inc(*args)
-
self.class.inc(*args)
-
end
-
-
#
-
# Initializes the instance and yields itself if called with a block.
-
#
-
# +banner+:: Banner message.
-
# +width+:: Summary width.
-
# +indent+:: Summary indent.
-
#
-
1
def initialize(banner = nil, width = 32, indent = ' ' * 4)
-
1
@stack = [DefaultList, List.new, List.new]
-
1
@program_name = nil
-
1
@banner = banner
-
1
@summary_width = width
-
1
@summary_indent = indent
-
1
@default_argv = ARGV
-
1
add_officious
-
1
yield self if block_given?
-
end
-
-
1
def add_officious # :nodoc:
-
1
list = base()
-
1
Officious.each do |opt, block|
-
4
list.long[opt] ||= block.call(self)
-
end
-
end
-
-
#
-
# Terminates option parsing. Optional parameter +arg+ is a string pushed
-
# back to be the first non-option argument.
-
#
-
1
def terminate(arg = nil)
-
self.class.terminate(arg)
-
end
-
1
def self.terminate(arg = nil)
-
throw :terminate, arg
-
end
-
-
1
@stack = [DefaultList]
-
14
def self.top() DefaultList end
-
-
#
-
# Directs to accept specified class +t+. The argument string is passed to
-
# the block in which it should be converted to the desired class.
-
#
-
# +t+:: Argument class specifier, any object including Class.
-
# +pat+:: Pattern for argument, defaults to +t+ if it responds to match.
-
#
-
# accept(t, pat, &block)
-
#
-
1
def accept(*args, &blk) top.accept(*args, &blk) end
-
#
-
# See #accept.
-
#
-
14
def self.accept(*args, &blk) top.accept(*args, &blk) end
-
-
#
-
# Directs to reject specified class argument.
-
#
-
# +t+:: Argument class specifier, any object including Class.
-
#
-
# reject(t)
-
#
-
1
def reject(*args, &blk) top.reject(*args, &blk) end
-
#
-
# See #reject.
-
#
-
1
def self.reject(*args, &blk) top.reject(*args, &blk) end
-
-
#
-
# Instance methods
-
#
-
-
# Heading banner preceding summary.
-
1
attr_writer :banner
-
-
# Program name to be emitted in error message and default banner,
-
# defaults to $0.
-
1
attr_writer :program_name
-
-
# Width for option list portion of summary. Must be Numeric.
-
1
attr_accessor :summary_width
-
-
# Indentation for summary. Must be String (or have + String method).
-
1
attr_accessor :summary_indent
-
-
# Strings to be parsed in default.
-
1
attr_accessor :default_argv
-
-
#
-
# Heading banner preceding summary.
-
#
-
1
def banner
-
unless @banner
-
@banner = +"Usage: #{program_name} [options]"
-
visit(:add_banner, @banner)
-
end
-
@banner
-
end
-
-
#
-
# Program name to be emitted in error message and default banner, defaults
-
# to $0.
-
#
-
1
def program_name
-
@program_name || File.basename($0, '.*')
-
end
-
-
# for experimental cascading :-)
-
1
alias set_banner banner=
-
1
alias set_program_name program_name=
-
1
alias set_summary_width summary_width=
-
1
alias set_summary_indent summary_indent=
-
-
# Version
-
1
attr_writer :version
-
# Release code
-
1
attr_writer :release
-
-
#
-
# Version
-
#
-
1
def version
-
(defined?(@version) && @version) || (defined?(::Version) && ::Version)
-
end
-
-
#
-
# Release code
-
#
-
1
def release
-
(defined?(@release) && @release) || (defined?(::Release) && ::Release) || (defined?(::RELEASE) && ::RELEASE)
-
end
-
-
#
-
# Returns version string from program_name, version and release.
-
#
-
1
def ver
-
if v = version
-
str = +"#{program_name} #{[v].join('.')}"
-
str << " (#{v})" if v = release
-
str
-
end
-
end
-
-
1
def warn(mesg = $!)
-
super("#{program_name}: #{mesg}")
-
end
-
-
1
def abort(mesg = $!)
-
super("#{program_name}: #{mesg}")
-
end
-
-
#
-
# Subject of #on / #on_head, #accept / #reject
-
#
-
1
def top
-
9
@stack[-1]
-
end
-
-
#
-
# Subject of #on_tail.
-
#
-
1
def base
-
1
@stack[1]
-
end
-
-
#
-
# Pushes a new List.
-
#
-
1
def new
-
@stack.push(List.new)
-
if block_given?
-
yield self
-
else
-
self
-
end
-
end
-
-
#
-
# Removes the last List.
-
#
-
1
def remove
-
@stack.pop
-
end
-
-
#
-
# Puts option summary into +to+ and returns +to+. Yields each line if
-
# a block is given.
-
#
-
# +to+:: Output destination, which must have method <<. Defaults to [].
-
# +width+:: Width of left side, defaults to @summary_width.
-
# +max+:: Maximum length allowed for left side, defaults to +width+ - 1.
-
# +indent+:: Indentation, defaults to @summary_indent.
-
#
-
1
def summarize(to = [], width = @summary_width, max = width - 1, indent = @summary_indent, &blk)
-
nl = "\n"
-
blk ||= proc {|l| to << (l.index(nl, -1) ? l : l + nl)}
-
visit(:summarize, {}, {}, width, max, indent, &blk)
-
to
-
end
-
-
#
-
# Returns option summary string.
-
#
-
1
def help; summarize("#{banner}".sub(/\n?\z/, "\n")) end
-
1
alias to_s help
-
-
#
-
# Returns option summary list.
-
#
-
1
def to_a; summarize("#{banner}".split(/^/)) end
-
-
#
-
# Checks if an argument is given twice, in which case an ArgumentError is
-
# raised. Called from OptionParser#switch only.
-
#
-
# +obj+:: New argument.
-
# +prv+:: Previously specified argument.
-
# +msg+:: Exception message.
-
#
-
1
def notwice(obj, prv, msg)
-
5
unless !prv or prv == obj
-
raise(ArgumentError, "argument #{msg} given twice: #{obj}",
-
ParseError.filter_backtrace(caller(2)))
-
end
-
5
obj
-
end
-
1
private :notwice
-
-
1
SPLAT_PROC = proc {|*a| a.length <= 1 ? a.first : a} # :nodoc:
-
#
-
# Creates an OptionParser::Switch from the parameters. The parsed argument
-
# value is passed to the given block, where it can be processed.
-
#
-
# See at the beginning of OptionParser for some full examples.
-
#
-
# +opts+ can include the following elements:
-
#
-
# [Argument style:]
-
# One of the following:
-
# :NONE, :REQUIRED, :OPTIONAL
-
#
-
# [Argument pattern:]
-
# Acceptable option argument format, must be pre-defined with
-
# OptionParser.accept or OptionParser#accept, or Regexp. This can appear
-
# once or assigned as String if not present, otherwise causes an
-
# ArgumentError. Examples:
-
# Float, Time, Array
-
#
-
# [Possible argument values:]
-
# Hash or Array.
-
# [:text, :binary, :auto]
-
# %w[iso-2022-jp shift_jis euc-jp utf8 binary]
-
# { "jis" => "iso-2022-jp", "sjis" => "shift_jis" }
-
#
-
# [Long style switch:]
-
# Specifies a long style switch which takes a mandatory, optional or no
-
# argument. It's a string of the following form:
-
# "--switch=MANDATORY" or "--switch MANDATORY"
-
# "--switch[=OPTIONAL]"
-
# "--switch"
-
#
-
# [Short style switch:]
-
# Specifies short style switch which takes a mandatory, optional or no
-
# argument. It's a string of the following form:
-
# "-xMANDATORY"
-
# "-x[OPTIONAL]"
-
# "-x"
-
# There is also a special form which matches character range (not full
-
# set of regular expression):
-
# "-[a-z]MANDATORY"
-
# "-[a-z][OPTIONAL]"
-
# "-[a-z]"
-
#
-
# [Argument style and description:]
-
# Instead of specifying mandatory or optional arguments directly in the
-
# switch parameter, this separate parameter can be used.
-
# "=MANDATORY"
-
# "=[OPTIONAL]"
-
#
-
# [Description:]
-
# Description string for the option.
-
# "Run verbosely"
-
# If you give multiple description strings, each string will be printed
-
# line by line.
-
#
-
# [Handler:]
-
# Handler for the parsed argument value. Either give a block or pass a
-
# Proc or Method as an argument.
-
#
-
1
def make_switch(opts, block = nil)
-
7
short, long, nolong, style, pattern, conv, not_pattern, not_conv, not_style = [], [], []
-
7
ldesc, sdesc, desc, arg = [], [], []
-
7
default_style = Switch::NoArgument
-
7
default_pattern = nil
-
7
klass = nil
-
7
q, a = nil
-
7
has_arg = false
-
-
7
opts.each do |o|
-
# argument class
-
21
next if search(:atype, o) do |pat, c|
-
1
klass = notwice(o, klass, 'type')
-
1
if not_style and not_style != Switch::NoArgument
-
not_pattern, not_conv = pat, c
-
else
-
1
default_pattern, conv = pat, c
-
end
-
end
-
-
# directly specified pattern(any object possible to match)
-
20
if (!(String === o || Symbol === o)) and o.respond_to?(:match)
-
pattern = notwice(o, pattern, 'pattern')
-
if pattern.respond_to?(:convert)
-
conv = pattern.method(:convert).to_proc
-
else
-
conv = SPLAT_PROC
-
end
-
next
-
end
-
-
# anything others
-
20
case o
-
when Proc, Method
-
block = notwice(o, block, 'block')
-
when Array, Hash
-
case pattern
-
when CompletingHash
-
when nil
-
pattern = CompletingHash.new
-
conv = pattern.method(:convert).to_proc if pattern.respond_to?(:convert)
-
else
-
raise ArgumentError, "argument pattern given twice"
-
end
-
o.each {|pat, *v| pattern[pat] = v.fetch(0) {pat}}
-
when Module
-
raise ArgumentError, "unsupported argument type: #{o}", ParseError.filter_backtrace(caller(4))
-
when *ArgumentStyle.keys
-
style = notwice(ArgumentStyle[o], style, 'style')
-
when /^--no-([^\[\]=\s]*)(.+)?/
-
1
q, a = $1, $2
-
1
o = notwice(a ? Object : TrueClass, klass, 'type')
-
1
not_pattern, not_conv = search(:atype, o) unless not_style
-
1
not_style = (not_style || default_style).guess(arg = a) if a
-
1
default_style = Switch::NoArgument
-
1
default_pattern, conv = search(:atype, FalseClass) unless default_pattern
-
1
ldesc << "--no-#{q}"
-
1
(q = q.downcase).tr!('_', '-')
-
1
long << "no-#{q}"
-
1
nolong << q
-
when /^--\[no-\]([^\[\]=\s]*)(.+)?/
-
q, a = $1, $2
-
o = notwice(a ? Object : TrueClass, klass, 'type')
-
if a
-
default_style = default_style.guess(arg = a)
-
default_pattern, conv = search(:atype, o) unless default_pattern
-
end
-
ldesc << "--[no-]#{q}"
-
(o = q.downcase).tr!('_', '-')
-
long << o
-
not_pattern, not_conv = search(:atype, FalseClass) unless not_style
-
not_style = Switch::NoArgument
-
nolong << "no-#{o}"
-
when /^--([^\[\]=\s]*)(.+)?/
-
6
q, a = $1, $2
-
6
if a
-
3
o = notwice(NilClass, klass, 'type')
-
3
default_style = default_style.guess(arg = a)
-
3
default_pattern, conv = search(:atype, o) unless default_pattern
-
end
-
6
ldesc << "--#{q}"
-
6
(o = q.downcase).tr!('_', '-')
-
6
long << o
-
when /^-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/
-
q, a = $1, $2
-
o = notwice(Object, klass, 'type')
-
if a
-
default_style = default_style.guess(arg = a)
-
default_pattern, conv = search(:atype, o) unless default_pattern
-
else
-
has_arg = true
-
end
-
sdesc << "-#{q}"
-
short << Regexp.new(q)
-
when /^-(.)(.+)?/
-
6
q, a = $1, $2
-
6
if a
-
o = notwice(NilClass, klass, 'type')
-
default_style = default_style.guess(arg = a)
-
default_pattern, conv = search(:atype, o) unless default_pattern
-
end
-
6
sdesc << "-#{q}"
-
6
short << q
-
when /^=/
-
style = notwice(default_style.guess(arg = o), style, 'style')
-
default_pattern, conv = search(:atype, Object) unless default_pattern
-
else
-
7
desc.push(o)
-
end
-
end
-
-
7
default_pattern, conv = search(:atype, default_style.pattern) unless default_pattern
-
7
if !(short.empty? and long.empty?)
-
7
if has_arg and default_style == Switch::NoArgument
-
default_style = Switch::RequiredArgument
-
end
-
7
s = (style || default_style).new(pattern || default_pattern,
-
conv, sdesc, ldesc, arg, desc, block)
-
elsif !block
-
if style or pattern
-
raise ArgumentError, "no switch given", ParseError.filter_backtrace(caller)
-
end
-
s = desc
-
else
-
short << pattern
-
s = (style || default_style).new(pattern,
-
conv, nil, nil, arg, desc, block)
-
end
-
7
return s, short, long,
-
7
(not_style.new(not_pattern, not_conv, sdesc, ldesc, nil, desc, block) if not_style),
-
nolong
-
end
-
-
1
def define(*opts, &block)
-
7
top.append(*(sw = make_switch(opts, block)))
-
7
sw[0]
-
end
-
-
#
-
# Add option switch and handler. See #make_switch for an explanation of
-
# parameters.
-
#
-
1
def on(*opts, &block)
-
7
define(*opts, &block)
-
7
self
-
end
-
1
alias def_option define
-
-
1
def define_head(*opts, &block)
-
top.prepend(*(sw = make_switch(opts, block)))
-
sw[0]
-
end
-
-
#
-
# Add option switch like with #on, but at head of summary.
-
#
-
1
def on_head(*opts, &block)
-
define_head(*opts, &block)
-
self
-
end
-
1
alias def_head_option define_head
-
-
1
def define_tail(*opts, &block)
-
base.append(*(sw = make_switch(opts, block)))
-
sw[0]
-
end
-
-
#
-
# Add option switch like with #on, but at tail of summary.
-
#
-
1
def on_tail(*opts, &block)
-
define_tail(*opts, &block)
-
self
-
end
-
1
alias def_tail_option define_tail
-
-
#
-
# Add separator in summary.
-
#
-
1
def separator(string)
-
2
top.append(string, nil, nil)
-
end
-
-
#
-
# Parses command line arguments +argv+ in order. When a block is given,
-
# each non-option argument is yielded. When optional +into+ keyword
-
# argument is provided, the parsed option values are stored there via
-
# <code>[]=</code> method (so it can be Hash, or OpenStruct, or other
-
# similar object).
-
#
-
# Returns the rest of +argv+ left unparsed.
-
#
-
1
def order(*argv, into: nil, &nonopt)
-
argv = argv[0].dup if argv.size == 1 and Array === argv[0]
-
order!(argv, into: into, &nonopt)
-
end
-
-
#
-
# Same as #order, but removes switches destructively.
-
# Non-option arguments remain in +argv+.
-
#
-
1
def order!(argv = default_argv, into: nil, &nonopt)
-
1
setter = ->(name, val) {into[name.to_sym] = val} if into
-
1
parse_in_order(argv, setter, &nonopt)
-
end
-
-
1
def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc:
-
1
opt, arg, val, rest = nil
-
1
nonopt ||= proc {|a| throw :terminate, a}
-
1
argv.unshift(arg) if arg = catch(:terminate) {
-
1
while arg = argv.shift
-
case arg
-
# long option
-
when /\A--([^=]*)(?:=(.*))?/m
-
opt, rest = $1, $2
-
opt.tr!('_', '-')
-
begin
-
sw, = complete(:long, opt, true)
-
rescue ParseError
-
raise $!.set_option(arg, true)
-
end
-
begin
-
opt, cb, val = sw.parse(rest, argv) {|*exc| raise(*exc)}
-
val = cb.call(val) if cb
-
setter.call(sw.switch_name, val) if setter
-
rescue ParseError
-
raise $!.set_option(arg, rest)
-
end
-
-
# short option
-
when /\A-(.)((=).*|.+)?/m
-
eq, rest, opt = $3, $2, $1
-
has_arg, val = eq, rest
-
begin
-
sw, = search(:short, opt)
-
unless sw
-
begin
-
sw, = complete(:short, opt)
-
# short option matched.
-
val = arg.delete_prefix('-')
-
has_arg = true
-
rescue InvalidOption
-
# if no short options match, try completion with long
-
# options.
-
sw, = complete(:long, opt)
-
eq ||= !rest
-
end
-
end
-
rescue ParseError
-
raise $!.set_option(arg, true)
-
end
-
begin
-
opt, cb, val = sw.parse(val, argv) {|*exc| raise(*exc) if eq}
-
raise InvalidOption, arg if has_arg and !eq and arg == "-#{opt}"
-
argv.unshift(opt) if opt and (!rest or (opt = opt.sub(/\A-*/, '-')) != '-')
-
val = cb.call(val) if cb
-
setter.call(sw.switch_name, val) if setter
-
rescue ParseError
-
raise $!.set_option(arg, arg.length > 2)
-
end
-
-
# non-option argument
-
else
-
catch(:prune) do
-
visit(:each_option) do |sw0|
-
sw = sw0
-
sw.block.call(arg) if Switch === sw and sw.match_nonswitch?(arg)
-
end
-
nonopt.call(arg)
-
end
-
end
-
end
-
-
1
nil
-
}
-
-
1
visit(:search, :short, nil) {|sw| sw.block.call(*argv) if !sw.pattern}
-
-
1
argv
-
end
-
1
private :parse_in_order
-
-
#
-
# Parses command line arguments +argv+ in permutation mode and returns
-
# list of non-option arguments. When optional +into+ keyword
-
# argument is provided, the parsed option values are stored there via
-
# <code>[]=</code> method (so it can be Hash, or OpenStruct, or other
-
# similar object).
-
#
-
1
def permute(*argv, into: nil)
-
argv = argv[0].dup if argv.size == 1 and Array === argv[0]
-
permute!(argv, into: into)
-
end
-
-
#
-
# Same as #permute, but removes switches destructively.
-
# Non-option arguments remain in +argv+.
-
#
-
1
def permute!(argv = default_argv, into: nil)
-
1
nonopts = []
-
1
order!(argv, into: into, &nonopts.method(:<<))
-
1
argv[0, 0] = nonopts
-
1
argv
-
end
-
-
#
-
# Parses command line arguments +argv+ in order when environment variable
-
# POSIXLY_CORRECT is set, and in permutation mode otherwise.
-
# When optional +into+ keyword argument is provided, the parsed option
-
# values are stored there via <code>[]=</code> method (so it can be Hash,
-
# or OpenStruct, or other similar object).
-
#
-
1
def parse(*argv, into: nil)
-
argv = argv[0].dup if argv.size == 1 and Array === argv[0]
-
parse!(argv, into: into)
-
end
-
-
#
-
# Same as #parse, but removes switches destructively.
-
# Non-option arguments remain in +argv+.
-
#
-
1
def parse!(argv = default_argv, into: nil)
-
1
if ENV.include?('POSIXLY_CORRECT')
-
order!(argv, into: into)
-
else
-
1
permute!(argv, into: into)
-
end
-
end
-
-
#
-
# Wrapper method for getopts.rb.
-
#
-
# params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option")
-
# # params["a"] = true # -a
-
# # params["b"] = "1" # -b1
-
# # params["foo"] = "1" # --foo
-
# # params["bar"] = "x" # --bar x
-
# # params["zot"] = "z" # --zot Z
-
#
-
1
def getopts(*args)
-
argv = Array === args.first ? args.shift : default_argv
-
single_options, *long_options = *args
-
-
result = {}
-
-
single_options.scan(/(.)(:)?/) do |opt, val|
-
if val
-
result[opt] = nil
-
define("-#{opt} VAL")
-
else
-
result[opt] = false
-
define("-#{opt}")
-
end
-
end if single_options
-
-
long_options.each do |arg|
-
arg, desc = arg.split(';', 2)
-
opt, val = arg.split(':', 2)
-
if val
-
result[opt] = val.empty? ? nil : val
-
define("--#{opt}=#{result[opt] || "VAL"}", *[desc].compact)
-
else
-
result[opt] = false
-
define("--#{opt}", *[desc].compact)
-
end
-
end
-
-
parse_in_order(argv, result.method(:[]=))
-
result
-
end
-
-
#
-
# See #getopts.
-
#
-
1
def self.getopts(*args)
-
new.getopts(*args)
-
end
-
-
#
-
# Traverses @stack, sending each element method +id+ with +args+ and
-
# +block+.
-
#
-
1
def visit(id, *args, &block)
-
30
@stack.reverse_each do |el|
-
90
el.send(id, *args, &block)
-
end
-
nil
-
end
-
1
private :visit
-
-
#
-
# Searches +key+ in @stack for +id+ hash and returns or yields the result.
-
#
-
1
def search(id, key)
-
29
block_given = block_given?
-
29
visit(:search, id, key) do |k|
-
9
return block_given ? yield(k) : k
-
end
-
end
-
1
private :search
-
-
#
-
# Completes shortened long style option switch and returns pair of
-
# canonical switch and switch descriptor OptionParser::Switch.
-
#
-
# +typ+:: Searching table.
-
# +opt+:: Searching key.
-
# +icase+:: Search case insensitive if true.
-
# +pat+:: Optional pattern for completion.
-
#
-
1
def complete(typ, opt, icase = false, *pat)
-
if pat.empty?
-
search(typ, opt) {|sw| return [sw, opt]} # exact match or...
-
end
-
ambiguous = catch(:ambiguous) {
-
visit(:complete, typ, opt, icase, *pat) {|o, *sw| return sw}
-
}
-
exc = ambiguous ? AmbiguousOption : InvalidOption
-
raise exc.new(opt, additional: self.method(:additional_message).curry[typ])
-
end
-
1
private :complete
-
-
#
-
# Returns additional info.
-
#
-
1
def additional_message(typ, opt)
-
return unless typ and opt and defined?(DidYouMean::SpellChecker)
-
all_candidates = []
-
visit(:get_candidates, typ) do |candidates|
-
all_candidates.concat(candidates)
-
end
-
all_candidates.select! {|cand| cand.is_a?(String) }
-
checker = DidYouMean::SpellChecker.new(dictionary: all_candidates)
-
DidYouMean.formatter.message_for(all_candidates & checker.correct(opt))
-
end
-
-
1
def candidate(word)
-
list = []
-
case word
-
when '-'
-
long = short = true
-
when /\A--/
-
word, arg = word.split(/=/, 2)
-
argpat = Completion.regexp(arg, false) if arg and !arg.empty?
-
long = true
-
when /\A-/
-
short = true
-
end
-
pat = Completion.regexp(word, long)
-
visit(:each_option) do |opt|
-
next unless Switch === opt
-
opts = (long ? opt.long : []) + (short ? opt.short : [])
-
opts = Completion.candidate(word, true, pat, &opts.method(:each)).map(&:first) if pat
-
if /\A=/ =~ opt.arg
-
opts.map! {|sw| sw + "="}
-
if arg and CompletingHash === opt.pattern
-
if opts = opt.pattern.candidate(arg, false, argpat)
-
opts.map!(&:last)
-
end
-
end
-
end
-
list.concat(opts)
-
end
-
list
-
end
-
-
#
-
# Loads options from file names as +filename+. Does nothing when the file
-
# is not present. Returns whether successfully loaded.
-
#
-
# +filename+ defaults to basename of the program without suffix in a
-
# directory ~/.options, then the basename with '.options' suffix
-
# under XDG and Haiku standard places.
-
#
-
1
def load(filename = nil)
-
unless filename
-
basename = File.basename($0, '.*')
-
return true if load(File.expand_path(basename, '~/.options')) rescue nil
-
basename << ".options"
-
return [
-
# XDG
-
ENV['XDG_CONFIG_HOME'],
-
'~/.config',
-
*ENV['XDG_CONFIG_DIRS']&.split(File::PATH_SEPARATOR),
-
-
# Haiku
-
'~/config/settings',
-
].any? {|dir|
-
next if !dir or dir.empty?
-
load(File.expand_path(basename, dir)) rescue nil
-
}
-
end
-
begin
-
parse(*IO.readlines(filename).each {|s| s.chomp!})
-
true
-
rescue Errno::ENOENT, Errno::ENOTDIR
-
false
-
end
-
end
-
-
#
-
# Parses environment variable +env+ or its uppercase with splitting like a
-
# shell.
-
#
-
# +env+ defaults to the basename of the program.
-
#
-
1
def environment(env = File.basename($0, '.*'))
-
env = ENV[env] || ENV[env.upcase] or return
-
require 'shellwords'
-
parse(*Shellwords.shellwords(env))
-
end
-
-
#
-
# Acceptable argument classes
-
#
-
-
#
-
# Any string and no conversion. This is fall-back.
-
#
-
1
accept(Object) {|s,|s or s.nil?}
-
-
1
accept(NilClass) {|s,|s}
-
-
#
-
# Any non-empty string, and no conversion.
-
#
-
1
accept(String, /.+/m) {|s,*|s}
-
-
#
-
# Ruby/C-like integer, octal for 0-7 sequence, binary for 0b, hexadecimal
-
# for 0x, and decimal for others; with optional sign prefix. Converts to
-
# Integer.
-
#
-
1
decimal = '\d+(?:_\d+)*'
-
1
binary = 'b[01]+(?:_[01]+)*'
-
1
hex = 'x[\da-f]+(?:_[\da-f]+)*'
-
1
octal = "0(?:[0-7]+(?:_[0-7]+)*|#{binary}|#{hex})?"
-
1
integer = "#{octal}|#{decimal}"
-
-
1
accept(Integer, %r"\A[-+]?(?:#{integer})\z"io) {|s,|
-
begin
-
Integer(s)
-
rescue ArgumentError
-
raise OptionParser::InvalidArgument, s
-
end if s
-
}
-
-
#
-
# Float number format, and converts to Float.
-
#
-
1
float = "(?:#{decimal}(?=(.)?)(?:\\.(?:#{decimal})?)?|\\.#{decimal})(?:E[-+]?#{decimal})?"
-
1
floatpat = %r"\A[-+]?#{float}\z"io
-
1
accept(Float, floatpat) {|s,| s.to_f if s}
-
-
#
-
# Generic numeric format, converts to Integer for integer format, Float
-
# for float format, and Rational for rational format.
-
#
-
1
real = "[-+]?(?:#{octal}|#{float})"
-
1
accept(Numeric, /\A(#{real})(?:\/(#{real}))?\z/io) {|s, d, f, n,|
-
if n
-
Rational(d, n)
-
elsif f
-
Float(s)
-
else
-
Integer(s)
-
end
-
}
-
-
#
-
# Decimal integer format, to be converted to Integer.
-
#
-
1
DecimalInteger = /\A[-+]?#{decimal}\z/io
-
1
accept(DecimalInteger, DecimalInteger) {|s,|
-
begin
-
Integer(s, 10)
-
rescue ArgumentError
-
raise OptionParser::InvalidArgument, s
-
end if s
-
}
-
-
#
-
# Ruby/C like octal/hexadecimal/binary integer format, to be converted to
-
# Integer.
-
#
-
1
OctalInteger = /\A[-+]?(?:[0-7]+(?:_[0-7]+)*|0(?:#{binary}|#{hex}))\z/io
-
1
accept(OctalInteger, OctalInteger) {|s,|
-
begin
-
Integer(s, 8)
-
rescue ArgumentError
-
raise OptionParser::InvalidArgument, s
-
end if s
-
}
-
-
#
-
# Decimal integer/float number format, to be converted to Integer for
-
# integer format, Float for float format.
-
#
-
1
DecimalNumeric = floatpat # decimal integer is allowed as float also.
-
1
accept(DecimalNumeric, floatpat) {|s, f|
-
begin
-
if f
-
Float(s)
-
else
-
Integer(s)
-
end
-
rescue ArgumentError
-
raise OptionParser::InvalidArgument, s
-
end if s
-
}
-
-
#
-
# Boolean switch, which means whether it is present or not, whether it is
-
# absent or not with prefix no-, or it takes an argument
-
# yes/no/true/false/+/-.
-
#
-
1
yesno = CompletingHash.new
-
4
%w[- no false].each {|el| yesno[el] = false}
-
4
%w[+ yes true].each {|el| yesno[el] = true}
-
1
yesno['nil'] = false # should be nil?
-
1
accept(TrueClass, yesno) {|arg, val| val == nil or val}
-
#
-
# Similar to TrueClass, but defaults to false.
-
#
-
1
accept(FalseClass, yesno) {|arg, val| val != nil and val}
-
-
#
-
# List of strings separated by ",".
-
#
-
1
accept(Array) do |s, |
-
if s
-
s = s.split(',').collect {|ss| ss unless ss.empty?}
-
end
-
s
-
end
-
-
#
-
# Regular expression with options.
-
#
-
1
accept(Regexp, %r"\A/((?:\\.|[^\\])*)/([[:alpha:]]+)?\z|.*") do |all, s, o|
-
f = 0
-
if o
-
f |= Regexp::IGNORECASE if /i/ =~ o
-
f |= Regexp::MULTILINE if /m/ =~ o
-
f |= Regexp::EXTENDED if /x/ =~ o
-
k = o.delete("imx")
-
k = nil if k.empty?
-
end
-
Regexp.new(s || all, f, k)
-
end
-
-
#
-
# Exceptions
-
#
-
-
#
-
# Base class of exceptions from OptionParser.
-
#
-
1
class ParseError < RuntimeError
-
# Reason which caused the error.
-
1
Reason = 'parse error'
-
-
1
def initialize(*args, additional: nil)
-
@additional = additional
-
@arg0, = args
-
@args = args
-
@reason = nil
-
end
-
-
1
attr_reader :args
-
1
attr_writer :reason
-
1
attr_accessor :additional
-
-
#
-
# Pushes back erred argument(s) to +argv+.
-
#
-
1
def recover(argv)
-
argv[0, 0] = @args
-
argv
-
end
-
-
1
def self.filter_backtrace(array)
-
unless $DEBUG
-
array.delete_if(&%r"\A#{Regexp.quote(__FILE__)}:"o.method(:=~))
-
end
-
array
-
end
-
-
1
def set_backtrace(array)
-
super(self.class.filter_backtrace(array))
-
end
-
-
1
def set_option(opt, eq)
-
if eq
-
@args[0] = opt
-
else
-
@args.unshift(opt)
-
end
-
self
-
end
-
-
#
-
# Returns error reason. Override this for I18N.
-
#
-
1
def reason
-
@reason || self.class::Reason
-
end
-
-
1
def inspect
-
"#<#{self.class}: #{args.join(' ')}>"
-
end
-
-
#
-
# Default stringizing method to emit standard error message.
-
#
-
1
def message
-
"#{reason}: #{args.join(' ')}#{additional[@arg0] if additional}"
-
end
-
-
1
alias to_s message
-
end
-
-
#
-
# Raises when ambiguously completable string is encountered.
-
#
-
1
class AmbiguousOption < ParseError
-
1
const_set(:Reason, 'ambiguous option')
-
end
-
-
#
-
# Raises when there is an argument for a switch which takes no argument.
-
#
-
1
class NeedlessArgument < ParseError
-
1
const_set(:Reason, 'needless argument')
-
end
-
-
#
-
# Raises when a switch with mandatory argument has no argument.
-
#
-
1
class MissingArgument < ParseError
-
1
const_set(:Reason, 'missing argument')
-
end
-
-
#
-
# Raises when switch is undefined.
-
#
-
1
class InvalidOption < ParseError
-
1
const_set(:Reason, 'invalid option')
-
end
-
-
#
-
# Raises when the given argument does not match required format.
-
#
-
1
class InvalidArgument < ParseError
-
1
const_set(:Reason, 'invalid argument')
-
end
-
-
#
-
# Raises when the given argument word can't be completed uniquely.
-
#
-
1
class AmbiguousArgument < InvalidArgument
-
1
const_set(:Reason, 'ambiguous argument')
-
end
-
-
#
-
# Miscellaneous
-
#
-
-
#
-
# Extends command line arguments array (ARGV) to parse itself.
-
#
-
1
module Arguable
-
-
#
-
# Sets OptionParser object, when +opt+ is +false+ or +nil+, methods
-
# OptionParser::Arguable#options and OptionParser::Arguable#options= are
-
# undefined. Thus, there is no ways to access the OptionParser object
-
# via the receiver object.
-
#
-
1
def options=(opt)
-
unless @optparse = opt
-
class << self
-
undef_method(:options)
-
undef_method(:options=)
-
end
-
end
-
end
-
-
#
-
# Actual OptionParser object, automatically created if nonexistent.
-
#
-
# If called with a block, yields the OptionParser object and returns the
-
# result of the block. If an OptionParser::ParseError exception occurs
-
# in the block, it is rescued, a error message printed to STDERR and
-
# +nil+ returned.
-
#
-
1
def options
-
@optparse ||= OptionParser.new
-
@optparse.default_argv = self
-
block_given? or return @optparse
-
begin
-
yield @optparse
-
rescue ParseError
-
@optparse.warn $!
-
nil
-
end
-
end
-
-
#
-
# Parses +self+ destructively in order and returns +self+ containing the
-
# rest arguments left unparsed.
-
#
-
1
def order!(&blk) options.order!(self, &blk) end
-
-
#
-
# Parses +self+ destructively in permutation mode and returns +self+
-
# containing the rest arguments left unparsed.
-
#
-
1
def permute!() options.permute!(self) end
-
-
#
-
# Parses +self+ destructively and returns +self+ containing the
-
# rest arguments left unparsed.
-
#
-
1
def parse!() options.parse!(self) end
-
-
#
-
# Substitution of getopts is possible as follows. Also see
-
# OptionParser#getopts.
-
#
-
# def getopts(*args)
-
# ($OPT = ARGV.getopts(*args)).each do |opt, val|
-
# eval "$OPT_#{opt.gsub(/[^A-Za-z0-9_]/, '_')} = val"
-
# end
-
# rescue OptionParser::ParseError
-
# end
-
#
-
1
def getopts(*args)
-
options.getopts(self, *args)
-
end
-
-
#
-
# Initializes instance variable.
-
#
-
1
def self.extend_object(obj)
-
1
super
-
2
obj.instance_eval {@optparse = nil}
-
end
-
1
def initialize(*args)
-
super
-
@optparse = nil
-
end
-
end
-
-
#
-
# Acceptable argument classes. Now contains DecimalInteger, OctalInteger
-
# and DecimalNumeric. See Acceptable argument classes (in source code).
-
#
-
1
module Acceptables
-
1
const_set(:DecimalInteger, OptionParser::DecimalInteger)
-
1
const_set(:OctalInteger, OptionParser::OctalInteger)
-
1
const_set(:DecimalNumeric, OptionParser::DecimalNumeric)
-
end
-
end
-
-
# ARGV is arguable by OptionParser
-
1
ARGV.extend(OptionParser::Arguable)
-
-
# An alias for OptionParser.
-
1
OptParse = OptionParser # :nodoc:
-
# frozen_string_literal: true
-
1
require 'psych/versions'
-
1
case RUBY_ENGINE
-
when 'jruby'
-
require 'psych_jars'
-
if JRuby::Util.respond_to?(:load_ext)
-
JRuby::Util.load_ext('org.jruby.ext.psych.PsychLibrary')
-
else
-
require 'java'; require 'jruby'
-
org.jruby.ext.psych.PsychLibrary.new.load(JRuby.runtime, false)
-
end
-
else
-
1
require 'psych.so'
-
end
-
1
require 'psych/nodes'
-
1
require 'psych/streaming'
-
1
require 'psych/visitors'
-
1
require 'psych/handler'
-
1
require 'psych/tree_builder'
-
1
require 'psych/parser'
-
1
require 'psych/omap'
-
1
require 'psych/set'
-
1
require 'psych/coder'
-
1
require 'psych/core_ext'
-
1
require 'psych/stream'
-
1
require 'psych/json/tree_builder'
-
1
require 'psych/json/stream'
-
1
require 'psych/handlers/document_stream'
-
1
require 'psych/class_loader'
-
-
###
-
# = Overview
-
#
-
# Psych is a YAML parser and emitter.
-
# Psych leverages libyaml [Home page: https://pyyaml.org/wiki/LibYAML]
-
# or [HG repo: https://bitbucket.org/xi/libyaml] for its YAML parsing
-
# and emitting capabilities. In addition to wrapping libyaml, Psych also
-
# knows how to serialize and de-serialize most Ruby objects to and from
-
# the YAML format.
-
#
-
# = I NEED TO PARSE OR EMIT YAML RIGHT NOW!
-
#
-
# # Parse some YAML
-
# Psych.load("--- foo") # => "foo"
-
#
-
# # Emit some YAML
-
# Psych.dump("foo") # => "--- foo\n...\n"
-
# { :a => 'b'}.to_yaml # => "---\n:a: b\n"
-
#
-
# Got more time on your hands? Keep on reading!
-
#
-
# == YAML Parsing
-
#
-
# Psych provides a range of interfaces for parsing a YAML document ranging from
-
# low level to high level, depending on your parsing needs. At the lowest
-
# level, is an event based parser. Mid level is access to the raw YAML AST,
-
# and at the highest level is the ability to unmarshal YAML to Ruby objects.
-
#
-
# == YAML Emitting
-
#
-
# Psych provides a range of interfaces ranging from low to high level for
-
# producing YAML documents. Very similar to the YAML parsing interfaces, Psych
-
# provides at the lowest level, an event based system, mid-level is building
-
# a YAML AST, and the highest level is converting a Ruby object straight to
-
# a YAML document.
-
#
-
# == High-level API
-
#
-
# === Parsing
-
#
-
# The high level YAML parser provided by Psych simply takes YAML as input and
-
# returns a Ruby data structure. For information on using the high level parser
-
# see Psych.load
-
#
-
# ==== Reading from a string
-
#
-
# Psych.load("--- a") # => 'a'
-
# Psych.load("---\n - a\n - b") # => ['a', 'b']
-
#
-
# ==== Reading from a file
-
#
-
# Psych.load_file("database.yml")
-
#
-
# ==== Exception handling
-
#
-
# begin
-
# # The second argument changes only the exception contents
-
# Psych.parse("--- `", "file.txt")
-
# rescue Psych::SyntaxError => ex
-
# ex.file # => 'file.txt'
-
# ex.message # => "(file.txt): found character that cannot start any token"
-
# end
-
#
-
# === Emitting
-
#
-
# The high level emitter has the easiest interface. Psych simply takes a Ruby
-
# data structure and converts it to a YAML document. See Psych.dump for more
-
# information on dumping a Ruby data structure.
-
#
-
# ==== Writing to a string
-
#
-
# # Dump an array, get back a YAML string
-
# Psych.dump(['a', 'b']) # => "---\n- a\n- b\n"
-
#
-
# # Dump an array to an IO object
-
# Psych.dump(['a', 'b'], StringIO.new) # => #<StringIO:0x000001009d0890>
-
#
-
# # Dump an array with indentation set
-
# Psych.dump(['a', ['b']], :indentation => 3) # => "---\n- a\n- - b\n"
-
#
-
# # Dump an array to an IO with indentation set
-
# Psych.dump(['a', ['b']], StringIO.new, :indentation => 3)
-
#
-
# ==== Writing to a file
-
#
-
# Currently there is no direct API for dumping Ruby structure to file:
-
#
-
# File.open('database.yml', 'w') do |file|
-
# file.write(Psych.dump(['a', 'b']))
-
# end
-
#
-
# == Mid-level API
-
#
-
# === Parsing
-
#
-
# Psych provides access to an AST produced from parsing a YAML document. This
-
# tree is built using the Psych::Parser and Psych::TreeBuilder. The AST can
-
# be examined and manipulated freely. Please see Psych::parse_stream,
-
# Psych::Nodes, and Psych::Nodes::Node for more information on dealing with
-
# YAML syntax trees.
-
#
-
# ==== Reading from a string
-
#
-
# # Returns Psych::Nodes::Stream
-
# Psych.parse_stream("---\n - a\n - b")
-
#
-
# # Returns Psych::Nodes::Document
-
# Psych.parse("---\n - a\n - b")
-
#
-
# ==== Reading from a file
-
#
-
# # Returns Psych::Nodes::Stream
-
# Psych.parse_stream(File.read('database.yml'))
-
#
-
# # Returns Psych::Nodes::Document
-
# Psych.parse_file('database.yml')
-
#
-
# ==== Exception handling
-
#
-
# begin
-
# # The second argument changes only the exception contents
-
# Psych.parse("--- `", "file.txt")
-
# rescue Psych::SyntaxError => ex
-
# ex.file # => 'file.txt'
-
# ex.message # => "(file.txt): found character that cannot start any token"
-
# end
-
#
-
# === Emitting
-
#
-
# At the mid level is building an AST. This AST is exactly the same as the AST
-
# used when parsing a YAML document. Users can build an AST by hand and the
-
# AST knows how to emit itself as a YAML document. See Psych::Nodes,
-
# Psych::Nodes::Node, and Psych::TreeBuilder for more information on building
-
# a YAML AST.
-
#
-
# ==== Writing to a string
-
#
-
# # We need Psych::Nodes::Stream (not Psych::Nodes::Document)
-
# stream = Psych.parse_stream("---\n - a\n - b")
-
#
-
# stream.to_yaml # => "---\n- a\n- b\n"
-
#
-
# ==== Writing to a file
-
#
-
# # We need Psych::Nodes::Stream (not Psych::Nodes::Document)
-
# stream = Psych.parse_stream(File.read('database.yml'))
-
#
-
# File.open('database.yml', 'w') do |file|
-
# file.write(stream.to_yaml)
-
# end
-
#
-
# == Low-level API
-
#
-
# === Parsing
-
#
-
# The lowest level parser should be used when the YAML input is already known,
-
# and the developer does not want to pay the price of building an AST or
-
# automatic detection and conversion to Ruby objects. See Psych::Parser for
-
# more information on using the event based parser.
-
#
-
# ==== Reading to Psych::Nodes::Stream structure
-
#
-
# parser = Psych::Parser.new(TreeBuilder.new) # => #<Psych::Parser>
-
# parser = Psych.parser # it's an alias for the above
-
#
-
# parser.parse("---\n - a\n - b") # => #<Psych::Parser>
-
# parser.handler # => #<Psych::TreeBuilder>
-
# parser.handler.root # => #<Psych::Nodes::Stream>
-
#
-
# ==== Receiving an events stream
-
#
-
# recorder = Psych::Handlers::Recorder.new
-
# parser = Psych::Parser.new(recorder)
-
#
-
# parser.parse("---\n - a\n - b")
-
# recorder.events # => [list of [event, args] lists]
-
# # event is one of: Psych::Handler::EVENTS
-
# # args are the arguments passed to the event
-
#
-
# === Emitting
-
#
-
# The lowest level emitter is an event based system. Events are sent to a
-
# Psych::Emitter object. That object knows how to convert the events to a YAML
-
# document. This interface should be used when document format is known in
-
# advance or speed is a concern. See Psych::Emitter for more information.
-
#
-
# ==== Writing to a Ruby structure
-
#
-
# Psych.parser.parse("--- a") # => #<Psych::Parser>
-
#
-
# parser.handler.first # => #<Psych::Nodes::Stream>
-
# parser.handler.first.to_ruby # => ["a"]
-
#
-
# parser.handler.root.first # => #<Psych::Nodes::Document>
-
# parser.handler.root.first.to_ruby # => "a"
-
#
-
# # You can instantiate an Emitter manually
-
# Psych::Visitors::ToRuby.new.accept(parser.handler.root.first)
-
# # => "a"
-
-
1
module Psych
-
# The version of libyaml Psych is using
-
1
LIBYAML_VERSION = Psych.libyaml_version.join '.'
-
# Deprecation guard
-
1
NOT_GIVEN = Object.new
-
1
private_constant :NOT_GIVEN
-
-
###
-
# Load +yaml+ in to a Ruby data structure. If multiple documents are
-
# provided, the object contained in the first document will be returned.
-
# +filename+ will be used in the exception message if any exception
-
# is raised while parsing. If +yaml+ is empty, it returns
-
# the specified +fallback+ return value, which defaults to +false+.
-
#
-
# Raises a Psych::SyntaxError when a YAML syntax error is detected.
-
#
-
# Example:
-
#
-
# Psych.load("--- a") # => 'a'
-
# Psych.load("---\n - a\n - b") # => ['a', 'b']
-
#
-
# begin
-
# Psych.load("--- `", filename: "file.txt")
-
# rescue Psych::SyntaxError => ex
-
# ex.file # => 'file.txt'
-
# ex.message # => "(file.txt): found character that cannot start any token"
-
# end
-
#
-
# When the optional +symbolize_names+ keyword argument is set to a
-
# true value, returns symbols for keys in Hash objects (default: strings).
-
#
-
# Psych.load("---\n foo: bar") # => {"foo"=>"bar"}
-
# Psych.load("---\n foo: bar", symbolize_names: true) # => {:foo=>"bar"}
-
#
-
# Raises a TypeError when `yaml` parameter is NilClass
-
#
-
# NOTE: This method *should not* be used to parse untrusted documents, such as
-
# YAML documents that are supplied via user input. Instead, please use the
-
# safe_load method.
-
#
-
1
def self.load yaml, legacy_filename = NOT_GIVEN, filename: nil, fallback: false, symbolize_names: false
-
if legacy_filename != NOT_GIVEN
-
warn_with_uplevel 'Passing filename with the 2nd argument of Psych.load is deprecated. Use keyword argument like Psych.load(yaml, filename: ...) instead.', uplevel: 1 if $VERBOSE
-
filename = legacy_filename
-
end
-
-
result = parse(yaml, filename: filename)
-
return fallback unless result
-
result = result.to_ruby if result
-
symbolize_names!(result) if symbolize_names
-
result
-
end
-
-
###
-
# Safely load the yaml string in +yaml+. By default, only the following
-
# classes are allowed to be deserialized:
-
#
-
# * TrueClass
-
# * FalseClass
-
# * NilClass
-
# * Numeric
-
# * String
-
# * Array
-
# * Hash
-
#
-
# Recursive data structures are not allowed by default. Arbitrary classes
-
# can be allowed by adding those classes to the +permitted_classes+ keyword argument. They are
-
# additive. For example, to allow Date deserialization:
-
#
-
# Psych.safe_load(yaml, permitted_classes: [Date])
-
#
-
# Now the Date class can be loaded in addition to the classes listed above.
-
#
-
# Aliases can be explicitly allowed by changing the +aliases+ keyword argument.
-
# For example:
-
#
-
# x = []
-
# x << x
-
# yaml = Psych.dump x
-
# Psych.safe_load yaml # => raises an exception
-
# Psych.safe_load yaml, aliases: true # => loads the aliases
-
#
-
# A Psych::DisallowedClass exception will be raised if the yaml contains a
-
# class that isn't in the +permitted_classes+ list.
-
#
-
# A Psych::BadAlias exception will be raised if the yaml contains aliases
-
# but the +aliases+ keyword argument is set to false.
-
#
-
# +filename+ will be used in the exception message if any exception is raised
-
# while parsing.
-
#
-
# When the optional +symbolize_names+ keyword argument is set to a
-
# true value, returns symbols for keys in Hash objects (default: strings).
-
#
-
# Psych.safe_load("---\n foo: bar") # => {"foo"=>"bar"}
-
# Psych.safe_load("---\n foo: bar", symbolize_names: true) # => {:foo=>"bar"}
-
#
-
1
def self.safe_load yaml, legacy_permitted_classes = NOT_GIVEN, legacy_permitted_symbols = NOT_GIVEN, legacy_aliases = NOT_GIVEN, legacy_filename = NOT_GIVEN, permitted_classes: [], permitted_symbols: [], aliases: false, filename: nil, fallback: nil, symbolize_names: false
-
if legacy_permitted_classes != NOT_GIVEN
-
warn_with_uplevel 'Passing permitted_classes with the 2nd argument of Psych.safe_load is deprecated. Use keyword argument like Psych.safe_load(yaml, permitted_classes: ...) instead.', uplevel: 1 if $VERBOSE
-
permitted_classes = legacy_permitted_classes
-
end
-
-
if legacy_permitted_symbols != NOT_GIVEN
-
warn_with_uplevel 'Passing permitted_symbols with the 3rd argument of Psych.safe_load is deprecated. Use keyword argument like Psych.safe_load(yaml, permitted_symbols: ...) instead.', uplevel: 1 if $VERBOSE
-
permitted_symbols = legacy_permitted_symbols
-
end
-
-
if legacy_aliases != NOT_GIVEN
-
warn_with_uplevel 'Passing aliases with the 4th argument of Psych.safe_load is deprecated. Use keyword argument like Psych.safe_load(yaml, aliases: ...) instead.', uplevel: 1 if $VERBOSE
-
aliases = legacy_aliases
-
end
-
-
if legacy_filename != NOT_GIVEN
-
warn_with_uplevel 'Passing filename with the 5th argument of Psych.safe_load is deprecated. Use keyword argument like Psych.safe_load(yaml, filename: ...) instead.', uplevel: 1 if $VERBOSE
-
filename = legacy_filename
-
end
-
-
result = parse(yaml, filename: filename)
-
return fallback unless result
-
-
class_loader = ClassLoader::Restricted.new(permitted_classes.map(&:to_s),
-
permitted_symbols.map(&:to_s))
-
scanner = ScalarScanner.new class_loader
-
visitor = if aliases
-
Visitors::ToRuby.new scanner, class_loader
-
else
-
Visitors::NoAliasRuby.new scanner, class_loader
-
end
-
result = visitor.accept result
-
symbolize_names!(result) if symbolize_names
-
result
-
end
-
-
###
-
# Parse a YAML string in +yaml+. Returns the Psych::Nodes::Document.
-
# +filename+ is used in the exception message if a Psych::SyntaxError is
-
# raised.
-
#
-
# Raises a Psych::SyntaxError when a YAML syntax error is detected.
-
#
-
# Example:
-
#
-
# Psych.parse("---\n - a\n - b") # => #<Psych::Nodes::Document:0x00>
-
#
-
# begin
-
# Psych.parse("--- `", filename: "file.txt")
-
# rescue Psych::SyntaxError => ex
-
# ex.file # => 'file.txt'
-
# ex.message # => "(file.txt): found character that cannot start any token"
-
# end
-
#
-
# See Psych::Nodes for more information about YAML AST.
-
1
def self.parse yaml, legacy_filename = NOT_GIVEN, filename: nil, fallback: NOT_GIVEN
-
if legacy_filename != NOT_GIVEN
-
warn_with_uplevel 'Passing filename with the 2nd argument of Psych.parse is deprecated. Use keyword argument like Psych.parse(yaml, filename: ...) instead.', uplevel: 1 if $VERBOSE
-
filename = legacy_filename
-
end
-
-
parse_stream(yaml, filename: filename) do |node|
-
return node
-
end
-
-
if fallback != NOT_GIVEN
-
warn_with_uplevel 'Passing the `fallback` keyword argument of Psych.parse is deprecated.', uplevel: 1 if $VERBOSE
-
fallback
-
else
-
false
-
end
-
end
-
-
###
-
# Parse a file at +filename+. Returns the Psych::Nodes::Document.
-
#
-
# Raises a Psych::SyntaxError when a YAML syntax error is detected.
-
1
def self.parse_file filename, fallback: false
-
result = File.open filename, 'r:bom|utf-8' do |f|
-
parse f, filename: filename
-
end
-
result || fallback
-
end
-
-
###
-
# Returns a default parser
-
1
def self.parser
-
Psych::Parser.new(TreeBuilder.new)
-
end
-
-
###
-
# Parse a YAML string in +yaml+. Returns the Psych::Nodes::Stream.
-
# This method can handle multiple YAML documents contained in +yaml+.
-
# +filename+ is used in the exception message if a Psych::SyntaxError is
-
# raised.
-
#
-
# If a block is given, a Psych::Nodes::Document node will be yielded to the
-
# block as it's being parsed.
-
#
-
# Raises a Psych::SyntaxError when a YAML syntax error is detected.
-
#
-
# Example:
-
#
-
# Psych.parse_stream("---\n - a\n - b") # => #<Psych::Nodes::Stream:0x00>
-
#
-
# Psych.parse_stream("--- a\n--- b") do |node|
-
# node # => #<Psych::Nodes::Document:0x00>
-
# end
-
#
-
# begin
-
# Psych.parse_stream("--- `", filename: "file.txt")
-
# rescue Psych::SyntaxError => ex
-
# ex.file # => 'file.txt'
-
# ex.message # => "(file.txt): found character that cannot start any token"
-
# end
-
#
-
# Raises a TypeError when NilClass is passed.
-
#
-
# See Psych::Nodes for more information about YAML AST.
-
1
def self.parse_stream yaml, legacy_filename = NOT_GIVEN, filename: nil, &block
-
if legacy_filename != NOT_GIVEN
-
warn_with_uplevel 'Passing filename with the 2nd argument of Psych.parse_stream is deprecated. Use keyword argument like Psych.parse_stream(yaml, filename: ...) instead.', uplevel: 1 if $VERBOSE
-
filename = legacy_filename
-
end
-
-
if block_given?
-
parser = Psych::Parser.new(Handlers::DocumentStream.new(&block))
-
parser.parse yaml, filename
-
else
-
parser = self.parser
-
parser.parse yaml, filename
-
parser.handler.root
-
end
-
end
-
-
###
-
# call-seq:
-
# Psych.dump(o) -> string of yaml
-
# Psych.dump(o, options) -> string of yaml
-
# Psych.dump(o, io) -> io object passed in
-
# Psych.dump(o, io, options) -> io object passed in
-
#
-
# Dump Ruby object +o+ to a YAML string. Optional +options+ may be passed in
-
# to control the output format. If an IO object is passed in, the YAML will
-
# be dumped to that IO object.
-
#
-
# Currently supported options are:
-
#
-
# [<tt>:indentation</tt>] Number of space characters used to indent.
-
# Acceptable value should be in <tt>0..9</tt> range,
-
# otherwise option is ignored.
-
#
-
# Default: <tt>2</tt>.
-
# [<tt>:line_width</tt>] Max character to wrap line at.
-
#
-
# Default: <tt>0</tt> (meaning "wrap at 81").
-
# [<tt>:canonical</tt>] Write "canonical" YAML form (very verbose, yet
-
# strictly formal).
-
#
-
# Default: <tt>false</tt>.
-
# [<tt>:header</tt>] Write <tt>%YAML [version]</tt> at the beginning of document.
-
#
-
# Default: <tt>false</tt>.
-
#
-
# Example:
-
#
-
# # Dump an array, get back a YAML string
-
# Psych.dump(['a', 'b']) # => "---\n- a\n- b\n"
-
#
-
# # Dump an array to an IO object
-
# Psych.dump(['a', 'b'], StringIO.new) # => #<StringIO:0x000001009d0890>
-
#
-
# # Dump an array with indentation set
-
# Psych.dump(['a', ['b']], indentation: 3) # => "---\n- a\n- - b\n"
-
#
-
# # Dump an array to an IO with indentation set
-
# Psych.dump(['a', ['b']], StringIO.new, indentation: 3)
-
1
def self.dump o, io = nil, options = {}
-
if Hash === io
-
options = io
-
io = nil
-
end
-
-
visitor = Psych::Visitors::YAMLTree.create options
-
visitor << o
-
visitor.tree.yaml io, options
-
end
-
-
###
-
# Dump a list of objects as separate documents to a document stream.
-
#
-
# Example:
-
#
-
# Psych.dump_stream("foo\n ", {}) # => "--- ! \"foo\\n \"\n--- {}\n"
-
1
def self.dump_stream *objects
-
visitor = Psych::Visitors::YAMLTree.create({})
-
objects.each do |o|
-
visitor << o
-
end
-
visitor.tree.yaml
-
end
-
-
###
-
# Dump Ruby +object+ to a JSON string.
-
1
def self.to_json object
-
visitor = Psych::Visitors::JSONTree.create
-
visitor << object
-
visitor.tree.yaml
-
end
-
-
###
-
# Load multiple documents given in +yaml+. Returns the parsed documents
-
# as a list. If a block is given, each document will be converted to Ruby
-
# and passed to the block during parsing
-
#
-
# Example:
-
#
-
# Psych.load_stream("--- foo\n...\n--- bar\n...") # => ['foo', 'bar']
-
#
-
# list = []
-
# Psych.load_stream("--- foo\n...\n--- bar\n...") do |ruby|
-
# list << ruby
-
# end
-
# list # => ['foo', 'bar']
-
#
-
1
def self.load_stream yaml, legacy_filename = NOT_GIVEN, filename: nil, fallback: []
-
if legacy_filename != NOT_GIVEN
-
warn_with_uplevel 'Passing filename with the 2nd argument of Psych.load_stream is deprecated. Use keyword argument like Psych.load_stream(yaml, filename: ...) instead.', uplevel: 1 if $VERBOSE
-
filename = legacy_filename
-
end
-
-
result = if block_given?
-
parse_stream(yaml, filename: filename) do |node|
-
yield node.to_ruby
-
end
-
else
-
parse_stream(yaml, filename: filename).children.map(&:to_ruby)
-
end
-
-
return fallback if result.is_a?(Array) && result.empty?
-
result
-
end
-
-
###
-
# Load the document contained in +filename+. Returns the yaml contained in
-
# +filename+ as a Ruby object, or if the file is empty, it returns
-
# the specified +fallback+ return value, which defaults to +false+.
-
1
def self.load_file filename, fallback: false
-
File.open(filename, 'r:bom|utf-8') { |f|
-
self.load f, filename: filename, fallback: fallback
-
}
-
end
-
-
# :stopdoc:
-
1
@domain_types = {}
-
1
def self.add_domain_type domain, type_tag, &block
-
key = ['tag', domain, type_tag].join ':'
-
@domain_types[key] = [key, block]
-
@domain_types["tag:#{type_tag}"] = [key, block]
-
end
-
-
1
def self.add_builtin_type type_tag, &block
-
domain = 'yaml.org,2002'
-
key = ['tag', domain, type_tag].join ':'
-
@domain_types[key] = [key, block]
-
end
-
-
1
def self.remove_type type_tag
-
@domain_types.delete type_tag
-
end
-
-
1
@load_tags = {}
-
1
@dump_tags = {}
-
1
def self.add_tag tag, klass
-
@load_tags[tag] = klass.name
-
@dump_tags[klass] = tag
-
end
-
-
1
def self.symbolize_names!(result)
-
case result
-
when Hash
-
result.keys.each do |key|
-
result[key.to_sym] = symbolize_names!(result.delete(key))
-
end
-
when Array
-
result.map! { |r| symbolize_names!(r) }
-
end
-
result
-
end
-
1
private_class_method :symbolize_names!
-
-
# Workaround for emulating `warn '...', uplevel: 1` in Ruby 2.4 or lower.
-
1
def self.warn_with_uplevel(message, uplevel: 1)
-
at = parse_caller(caller[uplevel]).join(':')
-
warn "#{at}: #{message}"
-
end
-
-
1
def self.parse_caller(at)
-
if /^(.+?):(\d+)(?::in `.*')?/ =~ at
-
file = $1
-
line = $2.to_i
-
[file, line]
-
end
-
end
-
1
private_class_method :warn_with_uplevel, :parse_caller
-
-
1
class << self
-
1
attr_accessor :load_tags
-
1
attr_accessor :dump_tags
-
1
attr_accessor :domain_types
-
end
-
# :startdoc:
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
###
-
# If an object defines +encode_with+, then an instance of Psych::Coder will
-
# be passed to the method when the object is being serialized. The Coder
-
# automatically assumes a Psych::Nodes::Mapping is being emitted. Other
-
# objects like Sequence and Scalar may be emitted if +seq=+ or +scalar=+ are
-
# called, respectively.
-
1
class Coder
-
1
attr_accessor :tag, :style, :implicit, :object
-
1
attr_reader :type, :seq
-
-
1
def initialize tag
-
@map = {}
-
@seq = []
-
@implicit = false
-
@type = :map
-
@tag = tag
-
@style = Psych::Nodes::Mapping::BLOCK
-
@scalar = nil
-
@object = nil
-
end
-
-
1
def scalar *args
-
if args.length > 0
-
warn "#{caller[0]}: Coder#scalar(a,b,c) is deprecated" if $VERBOSE
-
@tag, @scalar, _ = args
-
@type = :scalar
-
end
-
@scalar
-
end
-
-
# Emit a map. The coder will be yielded to the block.
-
1
def map tag = @tag, style = @style
-
@tag = tag
-
@style = style
-
yield self if block_given?
-
@map
-
end
-
-
# Emit a scalar with +value+ and +tag+
-
1
def represent_scalar tag, value
-
self.tag = tag
-
self.scalar = value
-
end
-
-
# Emit a sequence with +list+ and +tag+
-
1
def represent_seq tag, list
-
@tag = tag
-
self.seq = list
-
end
-
-
# Emit a sequence with +map+ and +tag+
-
1
def represent_map tag, map
-
@tag = tag
-
self.map = map
-
end
-
-
# Emit an arbitrary object +obj+ and +tag+
-
1
def represent_object tag, obj
-
@tag = tag
-
@type = :object
-
@object = obj
-
end
-
-
# Emit a scalar with +value+
-
1
def scalar= value
-
@type = :scalar
-
@scalar = value
-
end
-
-
# Emit a map with +value+
-
1
def map= map
-
@type = :map
-
@map = map
-
end
-
-
1
def []= k, v
-
@type = :map
-
@map[k] = v
-
end
-
1
alias :add :[]=
-
-
1
def [] k
-
@type = :map
-
@map[k]
-
end
-
-
# Emit a sequence of +list+
-
1
def seq= list
-
@type = :seq
-
@seq = list
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
class Object
-
1
def self.yaml_tag url
-
Psych.add_tag(url, self)
-
end
-
-
###
-
# call-seq: to_yaml(options = {})
-
#
-
# Convert an object to YAML. See Psych.dump for more information on the
-
# available +options+.
-
1
def to_yaml options = {}
-
Psych.dump self, options
-
end
-
end
-
-
1
if defined?(::IRB)
-
require 'psych/y'
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
1
class Exception < RuntimeError
-
end
-
-
1
class BadAlias < Exception
-
end
-
-
1
class DisallowedClass < Exception
-
1
def initialize klass_name
-
super "Tried to load unspecified class: #{klass_name}"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
###
-
# Psych::Handler is an abstract base class that defines the events used
-
# when dealing with Psych::Parser. Clients who want to use Psych::Parser
-
# should implement a class that inherits from Psych::Handler and define
-
# events that they can handle.
-
#
-
# Psych::Handler defines all events that Psych::Parser can possibly send to
-
# event handlers.
-
#
-
# See Psych::Parser for more details
-
1
class Handler
-
###
-
# Configuration options for dumping YAML.
-
1
class DumperOptions
-
1
attr_accessor :line_width, :indentation, :canonical
-
-
1
def initialize
-
1
@line_width = 0
-
1
@indentation = 2
-
1
@canonical = false
-
end
-
end
-
-
# Default dumping options
-
1
OPTIONS = DumperOptions.new
-
-
# Events that a Handler should respond to.
-
1
EVENTS = [ :alias,
-
:empty,
-
:end_document,
-
:end_mapping,
-
:end_sequence,
-
:end_stream,
-
:scalar,
-
:start_document,
-
:start_mapping,
-
:start_sequence,
-
:start_stream ]
-
-
###
-
# Called with +encoding+ when the YAML stream starts. This method is
-
# called once per stream. A stream may contain multiple documents.
-
#
-
# See the constants in Psych::Parser for the possible values of +encoding+.
-
1
def start_stream encoding
-
end
-
-
###
-
# Called when the document starts with the declared +version+,
-
# +tag_directives+, if the document is +implicit+.
-
#
-
# +version+ will be an array of integers indicating the YAML version being
-
# dealt with, +tag_directives+ is a list of tuples indicating the prefix
-
# and suffix of each tag, and +implicit+ is a boolean indicating whether
-
# the document is started implicitly.
-
#
-
# === Example
-
#
-
# Given the following YAML:
-
#
-
# %YAML 1.1
-
# %TAG ! tag:tenderlovemaking.com,2009:
-
# --- !squee
-
#
-
# The parameters for start_document must be this:
-
#
-
# version # => [1, 1]
-
# tag_directives # => [["!", "tag:tenderlovemaking.com,2009:"]]
-
# implicit # => false
-
1
def start_document version, tag_directives, implicit
-
end
-
-
###
-
# Called with the document ends. +implicit+ is a boolean value indicating
-
# whether or not the document has an implicit ending.
-
#
-
# === Example
-
#
-
# Given the following YAML:
-
#
-
# ---
-
# hello world
-
#
-
# +implicit+ will be true. Given this YAML:
-
#
-
# ---
-
# hello world
-
# ...
-
#
-
# +implicit+ will be false.
-
1
def end_document implicit
-
end
-
-
###
-
# Called when an alias is found to +anchor+. +anchor+ will be the name
-
# of the anchor found.
-
#
-
# === Example
-
#
-
# Here we have an example of an array that references itself in YAML:
-
#
-
# --- &ponies
-
# - first element
-
# - *ponies
-
#
-
# &ponies is the anchor, *ponies is the alias. In this case, alias is
-
# called with "ponies".
-
1
def alias anchor
-
end
-
-
###
-
# Called when a scalar +value+ is found. The scalar may have an
-
# +anchor+, a +tag+, be implicitly +plain+ or implicitly +quoted+
-
#
-
# +value+ is the string value of the scalar
-
# +anchor+ is an associated anchor or nil
-
# +tag+ is an associated tag or nil
-
# +plain+ is a boolean value
-
# +quoted+ is a boolean value
-
# +style+ is an integer idicating the string style
-
#
-
# See the constants in Psych::Nodes::Scalar for the possible values of
-
# +style+
-
#
-
# === Example
-
#
-
# Here is a YAML document that exercises most of the possible ways this
-
# method can be called:
-
#
-
# ---
-
# - !str "foo"
-
# - &anchor fun
-
# - many
-
# lines
-
# - |
-
# many
-
# newlines
-
#
-
# The above YAML document contains a list with four strings. Here are
-
# the parameters sent to this method in the same order:
-
#
-
# # value anchor tag plain quoted style
-
# ["foo", nil, "!str", false, false, 3 ]
-
# ["fun", "anchor", nil, true, false, 1 ]
-
# ["many lines", nil, nil, true, false, 1 ]
-
# ["many\nnewlines\n", nil, nil, false, true, 4 ]
-
#
-
1
def scalar value, anchor, tag, plain, quoted, style
-
end
-
-
###
-
# Called when a sequence is started.
-
#
-
# +anchor+ is the anchor associated with the sequence or nil.
-
# +tag+ is the tag associated with the sequence or nil.
-
# +implicit+ a boolean indicating whether or not the sequence was implicitly
-
# started.
-
# +style+ is an integer indicating the list style.
-
#
-
# See the constants in Psych::Nodes::Sequence for the possible values of
-
# +style+.
-
#
-
# === Example
-
#
-
# Here is a YAML document that exercises most of the possible ways this
-
# method can be called:
-
#
-
# ---
-
# - !!seq [
-
# a
-
# ]
-
# - &pewpew
-
# - b
-
#
-
# The above YAML document consists of three lists, an outer list that
-
# contains two inner lists. Here is a matrix of the parameters sent
-
# to represent these lists:
-
#
-
# # anchor tag implicit style
-
# [nil, nil, true, 1 ]
-
# [nil, "tag:yaml.org,2002:seq", false, 2 ]
-
# ["pewpew", nil, true, 1 ]
-
-
1
def start_sequence anchor, tag, implicit, style
-
end
-
-
###
-
# Called when a sequence ends.
-
1
def end_sequence
-
end
-
-
###
-
# Called when a map starts.
-
#
-
# +anchor+ is the anchor associated with the map or +nil+.
-
# +tag+ is the tag associated with the map or +nil+.
-
# +implicit+ is a boolean indicating whether or not the map was implicitly
-
# started.
-
# +style+ is an integer indicating the mapping style.
-
#
-
# See the constants in Psych::Nodes::Mapping for the possible values of
-
# +style+.
-
#
-
# === Example
-
#
-
# Here is a YAML document that exercises most of the possible ways this
-
# method can be called:
-
#
-
# ---
-
# k: !!map { hello: world }
-
# v: &pewpew
-
# hello: world
-
#
-
# The above YAML document consists of three maps, an outer map that contains
-
# two inner maps. Below is a matrix of the parameters sent in order to
-
# represent these three maps:
-
#
-
# # anchor tag implicit style
-
# [nil, nil, true, 1 ]
-
# [nil, "tag:yaml.org,2002:map", false, 2 ]
-
# ["pewpew", nil, true, 1 ]
-
-
1
def start_mapping anchor, tag, implicit, style
-
end
-
-
###
-
# Called when a map ends
-
1
def end_mapping
-
end
-
-
###
-
# Called when an empty event happens. (Which, as far as I can tell, is
-
# never).
-
1
def empty
-
end
-
-
###
-
# Called when the YAML stream ends
-
1
def end_stream
-
end
-
-
###
-
# Called before each event with line/column information.
-
1
def event_location(start_line, start_column, end_line, end_column)
-
end
-
-
###
-
# Is this handler a streaming handler?
-
1
def streaming?
-
false
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'psych/tree_builder'
-
-
1
module Psych
-
1
module Handlers
-
1
class DocumentStream < Psych::TreeBuilder # :nodoc:
-
1
def initialize &block
-
super
-
@block = block
-
end
-
-
1
def start_document version, tag_directives, implicit
-
n = Nodes::Document.new version, tag_directives, implicit
-
push n
-
end
-
-
1
def end_document implicit_end = !streaming?
-
@last.implicit_end = implicit_end
-
@block.call pop
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
1
module JSON
-
1
module RubyEvents # :nodoc:
-
1
def visit_Time o
-
formatted = format_time o
-
@emitter.scalar formatted, nil, nil, false, true, Nodes::Scalar::DOUBLE_QUOTED
-
end
-
-
1
def visit_DateTime o
-
visit_Time o.to_time
-
end
-
-
1
def visit_String o
-
@emitter.scalar o.to_s, nil, nil, false, true, Nodes::Scalar::DOUBLE_QUOTED
-
end
-
1
alias :visit_Symbol :visit_String
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'psych/json/ruby_events'
-
1
require 'psych/json/yaml_events'
-
-
1
module Psych
-
1
module JSON
-
1
class Stream < Psych::Visitors::JSONTree
-
1
include Psych::JSON::RubyEvents
-
1
include Psych::Streaming
-
1
extend Psych::Streaming::ClassMethods
-
-
1
class Emitter < Psych::Stream::Emitter # :nodoc:
-
1
include Psych::JSON::YAMLEvents
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'psych/json/yaml_events'
-
-
1
module Psych
-
1
module JSON
-
###
-
# Psych::JSON::TreeBuilder is an event based AST builder. Events are sent
-
# to an instance of Psych::JSON::TreeBuilder and a JSON AST is constructed.
-
1
class TreeBuilder < Psych::TreeBuilder
-
1
include Psych::JSON::YAMLEvents
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
1
module JSON
-
1
module YAMLEvents # :nodoc:
-
1
def start_document version, tag_directives, implicit
-
super(version, tag_directives, !streaming?)
-
end
-
-
1
def end_document implicit_end = !streaming?
-
super(implicit_end)
-
end
-
-
1
def start_mapping anchor, tag, implicit, style
-
super(anchor, nil, true, Nodes::Mapping::FLOW)
-
end
-
-
1
def start_sequence anchor, tag, implicit, style
-
super(anchor, nil, true, Nodes::Sequence::FLOW)
-
end
-
-
1
def scalar value, anchor, tag, plain, quoted, style
-
if "tag:yaml.org,2002:null" == tag
-
super('null', nil, nil, true, false, Nodes::Scalar::PLAIN)
-
else
-
super
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'psych/nodes/node'
-
1
require 'psych/nodes/stream'
-
1
require 'psych/nodes/document'
-
1
require 'psych/nodes/sequence'
-
1
require 'psych/nodes/scalar'
-
1
require 'psych/nodes/mapping'
-
1
require 'psych/nodes/alias'
-
-
1
module Psych
-
###
-
# = Overview
-
#
-
# When using Psych.load to deserialize a YAML document, the document is
-
# translated to an intermediary AST. That intermediary AST is then
-
# translated in to a Ruby object graph.
-
#
-
# In the opposite direction, when using Psych.dump, the Ruby object graph is
-
# translated to an intermediary AST which is then converted to a YAML
-
# document.
-
#
-
# Psych::Nodes contains all of the classes that make up the nodes of a YAML
-
# AST. You can manually build an AST and use one of the visitors (see
-
# Psych::Visitors) to convert that AST to either a YAML document or to a
-
# Ruby object graph.
-
#
-
# Here is an example of building an AST that represents a list with one
-
# scalar:
-
#
-
# # Create our nodes
-
# stream = Psych::Nodes::Stream.new
-
# doc = Psych::Nodes::Document.new
-
# seq = Psych::Nodes::Sequence.new
-
# scalar = Psych::Nodes::Scalar.new('foo')
-
#
-
# # Build up our tree
-
# stream.children << doc
-
# doc.children << seq
-
# seq.children << scalar
-
#
-
# The stream is the root of the tree. We can then convert the tree to YAML:
-
#
-
# stream.to_yaml => "---\n- foo\n"
-
#
-
# Or convert it to Ruby:
-
#
-
# stream.to_ruby => [["foo"]]
-
#
-
# == YAML AST Requirements
-
#
-
# A valid YAML AST *must* have one Psych::Nodes::Stream at the root. A
-
# Psych::Nodes::Stream node must have 1 or more Psych::Nodes::Document nodes
-
# as children.
-
#
-
# Psych::Nodes::Document nodes must have one and *only* one child. That child
-
# may be one of:
-
#
-
# * Psych::Nodes::Sequence
-
# * Psych::Nodes::Mapping
-
# * Psych::Nodes::Scalar
-
#
-
# Psych::Nodes::Sequence and Psych::Nodes::Mapping nodes may have many
-
# children, but Psych::Nodes::Mapping nodes should have an even number of
-
# children.
-
#
-
# All of these are valid children for Psych::Nodes::Sequence and
-
# Psych::Nodes::Mapping nodes:
-
#
-
# * Psych::Nodes::Sequence
-
# * Psych::Nodes::Mapping
-
# * Psych::Nodes::Scalar
-
# * Psych::Nodes::Alias
-
#
-
# Psych::Nodes::Scalar and Psych::Nodes::Alias are both terminal nodes and
-
# should not have any children.
-
1
module Nodes
-
end
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
1
module Nodes
-
###
-
# This class represents a {YAML Alias}[http://yaml.org/spec/1.1/#alias].
-
# It points to an +anchor+.
-
#
-
# A Psych::Nodes::Alias is a terminal node and may have no children.
-
1
class Alias < Psych::Nodes::Node
-
# The anchor this alias links to
-
1
attr_accessor :anchor
-
-
# Create a new Alias that points to an +anchor+
-
1
def initialize anchor
-
@anchor = anchor
-
end
-
-
1
def alias?; true; end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
1
module Nodes
-
###
-
# This represents a YAML Document. This node must be a child of
-
# Psych::Nodes::Stream. A Psych::Nodes::Document must have one child,
-
# and that child may be one of the following:
-
#
-
# * Psych::Nodes::Sequence
-
# * Psych::Nodes::Mapping
-
# * Psych::Nodes::Scalar
-
1
class Document < Psych::Nodes::Node
-
# The version of the YAML document
-
1
attr_accessor :version
-
-
# A list of tag directives for this document
-
1
attr_accessor :tag_directives
-
-
# Was this document implicitly created?
-
1
attr_accessor :implicit
-
-
# Is the end of the document implicit?
-
1
attr_accessor :implicit_end
-
-
###
-
# Create a new Psych::Nodes::Document object.
-
#
-
# +version+ is a list indicating the YAML version.
-
# +tags_directives+ is a list of tag directive declarations
-
# +implicit+ is a flag indicating whether the document will be implicitly
-
# started.
-
#
-
# == Example:
-
# This creates a YAML document object that represents a YAML 1.1 document
-
# with one tag directive, and has an implicit start:
-
#
-
# Psych::Nodes::Document.new(
-
# [1,1],
-
# [["!", "tag:tenderlovemaking.com,2009:"]],
-
# true
-
# )
-
#
-
# == See Also
-
# See also Psych::Handler#start_document
-
1
def initialize version = [], tag_directives = [], implicit = false
-
super()
-
@version = version
-
@tag_directives = tag_directives
-
@implicit = implicit
-
@implicit_end = true
-
end
-
-
###
-
# Returns the root node. A Document may only have one root node:
-
# http://yaml.org/spec/1.1/#id898031
-
1
def root
-
children.first
-
end
-
-
1
def document?; true; end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
1
module Nodes
-
###
-
# This class represents a {YAML Mapping}[http://yaml.org/spec/1.1/#mapping].
-
#
-
# A Psych::Nodes::Mapping node may have 0 or more children, but must have
-
# an even number of children. Here are the valid children a
-
# Psych::Nodes::Mapping node may have:
-
#
-
# * Psych::Nodes::Sequence
-
# * Psych::Nodes::Mapping
-
# * Psych::Nodes::Scalar
-
# * Psych::Nodes::Alias
-
1
class Mapping < Psych::Nodes::Node
-
# Any Map Style
-
1
ANY = 0
-
-
# Block Map Style
-
1
BLOCK = 1
-
-
# Flow Map Style
-
1
FLOW = 2
-
-
# The optional anchor for this mapping
-
1
attr_accessor :anchor
-
-
# The optional tag for this mapping
-
1
attr_accessor :tag
-
-
# Is this an implicit mapping?
-
1
attr_accessor :implicit
-
-
# The style of this mapping
-
1
attr_accessor :style
-
-
###
-
# Create a new Psych::Nodes::Mapping object.
-
#
-
# +anchor+ is the anchor associated with the map or +nil+.
-
# +tag+ is the tag associated with the map or +nil+.
-
# +implicit+ is a boolean indicating whether or not the map was implicitly
-
# started.
-
# +style+ is an integer indicating the mapping style.
-
#
-
# == See Also
-
# See also Psych::Handler#start_mapping
-
1
def initialize anchor = nil, tag = nil, implicit = true, style = BLOCK
-
super()
-
@anchor = anchor
-
@tag = tag
-
@implicit = implicit
-
@style = style
-
end
-
-
1
def mapping?; true; end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'stringio'
-
1
require 'psych/class_loader'
-
1
require 'psych/scalar_scanner'
-
-
1
module Psych
-
1
module Nodes
-
###
-
# The base class for any Node in a YAML parse tree. This class should
-
# never be instantiated.
-
1
class Node
-
1
include Enumerable
-
-
# The children of this node
-
1
attr_reader :children
-
-
# An associated tag
-
1
attr_reader :tag
-
-
# The line number where this node start
-
1
attr_accessor :start_line
-
-
# The column number where this node start
-
1
attr_accessor :start_column
-
-
# The line number where this node ends
-
1
attr_accessor :end_line
-
-
# The column number where this node ends
-
1
attr_accessor :end_column
-
-
# Create a new Psych::Nodes::Node
-
1
def initialize
-
@children = []
-
end
-
-
###
-
# Iterate over each node in the tree. Yields each node to +block+ depth
-
# first.
-
1
def each &block
-
return enum_for :each unless block_given?
-
Visitors::DepthFirst.new(block).accept self
-
end
-
-
###
-
# Convert this node to Ruby.
-
#
-
# See also Psych::Visitors::ToRuby
-
1
def to_ruby
-
Visitors::ToRuby.create.accept(self)
-
end
-
1
alias :transform :to_ruby
-
-
###
-
# Convert this node to YAML.
-
#
-
# See also Psych::Visitors::Emitter
-
1
def yaml io = nil, options = {}
-
real_io = io || StringIO.new(''.encode('utf-8'))
-
-
Visitors::Emitter.new(real_io, options).accept self
-
return real_io.string unless io
-
io
-
end
-
1
alias :to_yaml :yaml
-
-
1
def alias?; false; end
-
1
def document?; false; end
-
1
def mapping?; false; end
-
1
def scalar?; false; end
-
1
def sequence?; false; end
-
1
def stream?; false; end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
1
module Nodes
-
###
-
# This class represents a {YAML Scalar}[http://yaml.org/spec/1.1/#id858081].
-
#
-
# This node type is a terminal node and should not have any children.
-
1
class Scalar < Psych::Nodes::Node
-
# Any style scalar, the emitter chooses
-
1
ANY = 0
-
-
# Plain scalar style
-
1
PLAIN = 1
-
-
# Single quoted style
-
1
SINGLE_QUOTED = 2
-
-
# Double quoted style
-
1
DOUBLE_QUOTED = 3
-
-
# Literal style
-
1
LITERAL = 4
-
-
# Folded style
-
1
FOLDED = 5
-
-
# The scalar value
-
1
attr_accessor :value
-
-
# The anchor value (if there is one)
-
1
attr_accessor :anchor
-
-
# The tag value (if there is one)
-
1
attr_accessor :tag
-
-
# Is this a plain scalar?
-
1
attr_accessor :plain
-
-
# Is this scalar quoted?
-
1
attr_accessor :quoted
-
-
# The style of this scalar
-
1
attr_accessor :style
-
-
###
-
# Create a new Psych::Nodes::Scalar object.
-
#
-
# +value+ is the string value of the scalar
-
# +anchor+ is an associated anchor or nil
-
# +tag+ is an associated tag or nil
-
# +plain+ is a boolean value
-
# +quoted+ is a boolean value
-
# +style+ is an integer idicating the string style
-
#
-
# == See Also
-
#
-
# See also Psych::Handler#scalar
-
1
def initialize value, anchor = nil, tag = nil, plain = true, quoted = false, style = ANY
-
@value = value
-
@anchor = anchor
-
@tag = tag
-
@plain = plain
-
@quoted = quoted
-
@style = style
-
end
-
-
1
def scalar?; true; end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
1
module Nodes
-
###
-
# This class represents a
-
# {YAML sequence}[http://yaml.org/spec/1.1/#sequence/syntax].
-
#
-
# A YAML sequence is basically a list, and looks like this:
-
#
-
# %YAML 1.1
-
# ---
-
# - I am
-
# - a Sequence
-
#
-
# A YAML sequence may have an anchor like this:
-
#
-
# %YAML 1.1
-
# ---
-
# &A [
-
# "This sequence",
-
# "has an anchor"
-
# ]
-
#
-
# A YAML sequence may also have a tag like this:
-
#
-
# %YAML 1.1
-
# ---
-
# !!seq [
-
# "This sequence",
-
# "has a tag"
-
# ]
-
#
-
# This class represents a sequence in a YAML document. A
-
# Psych::Nodes::Sequence node may have 0 or more children. Valid children
-
# for this node are:
-
#
-
# * Psych::Nodes::Sequence
-
# * Psych::Nodes::Mapping
-
# * Psych::Nodes::Scalar
-
# * Psych::Nodes::Alias
-
1
class Sequence < Psych::Nodes::Node
-
# Any Styles, emitter chooses
-
1
ANY = 0
-
-
# Block style sequence
-
1
BLOCK = 1
-
-
# Flow style sequence
-
1
FLOW = 2
-
-
# The anchor for this sequence (if any)
-
1
attr_accessor :anchor
-
-
# The tag name for this sequence (if any)
-
1
attr_accessor :tag
-
-
# Is this sequence started implicitly?
-
1
attr_accessor :implicit
-
-
# The sequence style used
-
1
attr_accessor :style
-
-
###
-
# Create a new object representing a YAML sequence.
-
#
-
# +anchor+ is the anchor associated with the sequence or nil.
-
# +tag+ is the tag associated with the sequence or nil.
-
# +implicit+ a boolean indicating whether or not the sequence was
-
# implicitly started.
-
# +style+ is an integer indicating the list style.
-
#
-
# See Psych::Handler#start_sequence
-
1
def initialize anchor = nil, tag = nil, implicit = true, style = BLOCK
-
super()
-
@anchor = anchor
-
@tag = tag
-
@implicit = implicit
-
@style = style
-
end
-
-
1
def sequence?; true; end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
1
module Nodes
-
###
-
# Represents a YAML stream. This is the root node for any YAML parse
-
# tree. This node must have one or more child nodes. The only valid
-
# child node for a Psych::Nodes::Stream node is Psych::Nodes::Document.
-
1
class Stream < Psych::Nodes::Node
-
-
# Encodings supported by Psych (and libyaml)
-
-
# Any encoding
-
1
ANY = Psych::Parser::ANY
-
-
# UTF-8 encoding
-
1
UTF8 = Psych::Parser::UTF8
-
-
# UTF-16LE encoding
-
1
UTF16LE = Psych::Parser::UTF16LE
-
-
# UTF-16BE encoding
-
1
UTF16BE = Psych::Parser::UTF16BE
-
-
# The encoding used for this stream
-
1
attr_accessor :encoding
-
-
###
-
# Create a new Psych::Nodes::Stream node with an +encoding+ that
-
# defaults to Psych::Nodes::Stream::UTF8.
-
#
-
# See also Psych::Handler#start_stream
-
1
def initialize encoding = UTF8
-
super()
-
@encoding = encoding
-
end
-
-
1
def stream?; true; end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
1
class Omap < ::Hash
-
end
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
###
-
# YAML event parser class. This class parses a YAML document and calls
-
# events on the handler that is passed to the constructor. The events can
-
# be used for things such as constructing a YAML AST or deserializing YAML
-
# documents. It can even be fed back to Psych::Emitter to emit the same
-
# document that was parsed.
-
#
-
# See Psych::Handler for documentation on the events that Psych::Parser emits.
-
#
-
# Here is an example that prints out ever scalar found in a YAML document:
-
#
-
# # Handler for detecting scalar values
-
# class ScalarHandler < Psych::Handler
-
# def scalar value, anchor, tag, plain, quoted, style
-
# puts value
-
# end
-
# end
-
#
-
# parser = Psych::Parser.new(ScalarHandler.new)
-
# parser.parse(yaml_document)
-
#
-
# Here is an example that feeds the parser back in to Psych::Emitter. The
-
# YAML document is read from STDIN and written back out to STDERR:
-
#
-
# parser = Psych::Parser.new(Psych::Emitter.new($stderr))
-
# parser.parse($stdin)
-
#
-
# Psych uses Psych::Parser in combination with Psych::TreeBuilder to
-
# construct an AST of the parsed YAML document.
-
-
1
class Parser
-
1
class Mark < Struct.new(:index, :line, :column)
-
end
-
-
# The handler on which events will be called
-
1
attr_accessor :handler
-
-
# Set the encoding for this parser to +encoding+
-
1
attr_writer :external_encoding
-
-
###
-
# Creates a new Psych::Parser instance with +handler+. YAML events will
-
# be called on +handler+. See Psych::Parser for more details.
-
-
1
def initialize handler = Handler.new
-
@handler = handler
-
@external_encoding = ANY
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'strscan'
-
-
1
module Psych
-
###
-
# Scan scalars for built in types
-
1
class ScalarScanner
-
# Taken from http://yaml.org/type/timestamp.html
-
1
TIME = /^-?\d{4}-\d{1,2}-\d{1,2}(?:[Tt]|\s+)\d{1,2}:\d\d:\d\d(?:\.\d*)?(?:\s*(?:Z|[-+]\d{1,2}:?(?:\d\d)?))?$/
-
-
# Taken from http://yaml.org/type/float.html
-
1
FLOAT = /^(?:[-+]?([0-9][0-9_,]*)?\.[0-9]*([eE][-+][0-9]+)?(?# base 10)
-
|[-+]?\.(inf|Inf|INF)(?# infinity)
-
|\.(nan|NaN|NAN)(?# not a number))$/x
-
-
# Taken from http://yaml.org/type/int.html
-
1
INTEGER = /^(?:[-+]?0b[0-1_,]+ (?# base 2)
-
|[-+]?0[0-7_,]+ (?# base 8)
-
|[-+]?(?:0|[1-9][0-9_,]*) (?# base 10)
-
|[-+]?0x[0-9a-fA-F_,]+ (?# base 16))$/x
-
-
1
attr_reader :class_loader
-
-
# Create a new scanner
-
1
def initialize class_loader
-
@symbol_cache = {}
-
@class_loader = class_loader
-
end
-
-
# Tokenize +string+ returning the Ruby object
-
1
def tokenize string
-
return nil if string.empty?
-
return @symbol_cache[string] if @symbol_cache.key?(string)
-
-
# Check for a String type, being careful not to get caught by hash keys, hex values, and
-
# special floats (e.g., -.inf).
-
if string.match?(/^[^\d\.:-]?[A-Za-z_\s!@#\$%\^&\*\(\)\{\}\<\>\|\/\\~;=]+/) || string.match?(/\n/)
-
return string if string.length > 5
-
-
if string.match?(/^[^ytonf~]/i)
-
string
-
elsif string == '~' || string.match?(/^null$/i)
-
nil
-
elsif string.match?(/^(yes|true|on)$/i)
-
true
-
elsif string.match?(/^(no|false|off)$/i)
-
false
-
else
-
string
-
end
-
elsif string.match?(TIME)
-
begin
-
parse_time string
-
rescue ArgumentError
-
string
-
end
-
elsif string.match?(/^\d{4}-(?:1[012]|0\d|\d)-(?:[12]\d|3[01]|0\d|\d)$/)
-
require 'date'
-
begin
-
class_loader.date.strptime(string, '%Y-%m-%d')
-
rescue ArgumentError
-
string
-
end
-
elsif string.match?(/^\.inf$/i)
-
Float::INFINITY
-
elsif string.match?(/^-\.inf$/i)
-
-Float::INFINITY
-
elsif string.match?(/^\.nan$/i)
-
Float::NAN
-
elsif string.match?(/^:./)
-
if string =~ /^:(["'])(.*)\1/
-
@symbol_cache[string] = class_loader.symbolize($2.sub(/^:/, ''))
-
else
-
@symbol_cache[string] = class_loader.symbolize(string.sub(/^:/, ''))
-
end
-
elsif string.match?(/^[-+]?[0-9][0-9_]*(:[0-5]?[0-9]){1,2}$/)
-
i = 0
-
string.split(':').each_with_index do |n,e|
-
i += (n.to_i * 60 ** (e - 2).abs)
-
end
-
i
-
elsif string.match?(/^[-+]?[0-9][0-9_]*(:[0-5]?[0-9]){1,2}\.[0-9_]*$/)
-
i = 0
-
string.split(':').each_with_index do |n,e|
-
i += (n.to_f * 60 ** (e - 2).abs)
-
end
-
i
-
elsif string.match?(FLOAT)
-
if string.match?(/\A[-+]?\.\Z/)
-
string
-
else
-
Float(string.gsub(/[,_]|\.([Ee]|$)/, '\1'))
-
end
-
elsif string.match?(INTEGER)
-
parse_int string
-
else
-
string
-
end
-
end
-
-
###
-
# Parse and return an int from +string+
-
1
def parse_int string
-
Integer(string.gsub(/[,]/, ''))
-
end
-
-
###
-
# Parse and return a Time from +string+
-
1
def parse_time string
-
klass = class_loader.load 'Time'
-
-
date, time = *(string.split(/[ tT]/, 2))
-
(yy, m, dd) = date.match(/^(-?\d{4})-(\d{1,2})-(\d{1,2})/).captures.map { |x| x.to_i }
-
md = time.match(/(\d+:\d+:\d+)(?:\.(\d*))?\s*(Z|[-+]\d+(:\d\d)?)?/)
-
-
(hh, mm, ss) = md[1].split(':').map { |x| x.to_i }
-
us = (md[2] ? Rational("0.#{md[2]}") : 0) * 1000000
-
-
time = klass.utc(yy, m, dd, hh, mm, ss, us)
-
-
return time if 'Z' == md[3]
-
return klass.at(time.to_i, us) unless md[3]
-
-
tz = md[3].match(/^([+\-]?\d{1,2})\:?(\d{1,2})?$/)[1..-1].compact.map { |digit| Integer(digit, 10) }
-
offset = tz.first * 3600
-
-
if offset < 0
-
offset -= ((tz[1] || 0) * 60)
-
else
-
offset += ((tz[1] || 0) * 60)
-
end
-
-
klass.new(yy, m, dd, hh, mm, ss+us/(1_000_000r), offset)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
1
class Set < ::Hash
-
end
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
###
-
# Psych::Stream is a streaming YAML emitter. It will not buffer your YAML,
-
# but send it straight to an IO.
-
#
-
# Here is an example use:
-
#
-
# stream = Psych::Stream.new($stdout)
-
# stream.start
-
# stream.push({:foo => 'bar'})
-
# stream.finish
-
#
-
# YAML will be immediately emitted to $stdout with no buffering.
-
#
-
# Psych::Stream#start will take a block and ensure that Psych::Stream#finish
-
# is called, so you can do this form:
-
#
-
# stream = Psych::Stream.new($stdout)
-
# stream.start do |em|
-
# em.push(:foo => 'bar')
-
# end
-
#
-
1
class Stream < Psych::Visitors::YAMLTree
-
1
class Emitter < Psych::Emitter # :nodoc:
-
1
def end_document implicit_end = !streaming?
-
super
-
end
-
-
1
def streaming?
-
true
-
end
-
end
-
-
1
include Psych::Streaming
-
1
extend Psych::Streaming::ClassMethods
-
end
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
1
module Streaming
-
1
module ClassMethods
-
###
-
# Create a new streaming emitter. Emitter will print to +io+. See
-
# Psych::Stream for an example.
-
1
def new io
-
emitter = const_get(:Emitter).new(io)
-
class_loader = ClassLoader.new
-
ss = ScalarScanner.new class_loader
-
super(emitter, ss, {})
-
end
-
end
-
-
###
-
# Start streaming using +encoding+
-
1
def start encoding = Nodes::Stream::UTF8
-
super.tap { yield self if block_given? }
-
ensure
-
finish if block_given?
-
end
-
-
1
private
-
1
def register target, obj
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'psych/exception'
-
-
1
module Psych
-
1
class SyntaxError < Psych::Exception
-
1
attr_reader :file, :line, :column, :offset, :problem, :context
-
-
1
def initialize file, line, col, offset, problem, context
-
err = [problem, context].compact.join ' '
-
filename = file || '<unknown>'
-
message = "(%s): %s at line %d column %d" % [filename, err, line, col]
-
-
@file = file
-
@line = line
-
@column = col
-
@offset = offset
-
@problem = problem
-
@context = context
-
super(message)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'psych/handler'
-
-
1
module Psych
-
###
-
# This class works in conjunction with Psych::Parser to build an in-memory
-
# parse tree that represents a YAML document.
-
#
-
# == Example
-
#
-
# parser = Psych::Parser.new Psych::TreeBuilder.new
-
# parser.parse('--- foo')
-
# tree = parser.handler.root
-
#
-
# See Psych::Handler for documentation on the event methods used in this
-
# class.
-
1
class TreeBuilder < Psych::Handler
-
# Returns the root node for the built tree
-
1
attr_reader :root
-
-
# Create a new TreeBuilder instance
-
1
def initialize
-
@stack = []
-
@last = nil
-
@root = nil
-
-
@start_line = nil
-
@start_column = nil
-
@end_line = nil
-
@end_column = nil
-
end
-
-
1
def event_location(start_line, start_column, end_line, end_column)
-
@start_line = start_line
-
@start_column = start_column
-
@end_line = end_line
-
@end_column = end_column
-
end
-
-
1
%w{
-
Sequence
-
Mapping
-
}.each do |node|
-
2
class_eval %{
-
def start_#{node.downcase}(anchor, tag, implicit, style)
-
n = Nodes::#{node}.new(anchor, tag, implicit, style)
-
set_start_location(n)
-
@last.children << n
-
push n
-
end
-
-
def end_#{node.downcase}
-
n = pop
-
set_end_location(n)
-
n
-
end
-
}
-
end
-
-
###
-
# Handles start_document events with +version+, +tag_directives+,
-
# and +implicit+ styling.
-
#
-
# See Psych::Handler#start_document
-
1
def start_document version, tag_directives, implicit
-
n = Nodes::Document.new version, tag_directives, implicit
-
set_start_location(n)
-
@last.children << n
-
push n
-
end
-
-
###
-
# Handles end_document events with +version+, +tag_directives+,
-
# and +implicit+ styling.
-
#
-
# See Psych::Handler#start_document
-
1
def end_document implicit_end = !streaming?
-
@last.implicit_end = implicit_end
-
n = pop
-
set_end_location(n)
-
n
-
end
-
-
1
def start_stream encoding
-
@root = Nodes::Stream.new(encoding)
-
set_start_location(@root)
-
push @root
-
end
-
-
1
def end_stream
-
n = pop
-
set_end_location(n)
-
n
-
end
-
-
1
def scalar value, anchor, tag, plain, quoted, style
-
s = Nodes::Scalar.new(value,anchor,tag,plain,quoted,style)
-
set_location(s)
-
@last.children << s
-
s
-
end
-
-
1
def alias anchor
-
a = Nodes::Alias.new(anchor)
-
set_location(a)
-
@last.children << a
-
a
-
end
-
-
1
private
-
1
def push value
-
@stack.push value
-
@last = value
-
end
-
-
1
def pop
-
x = @stack.pop
-
@last = @stack.last
-
x
-
end
-
-
1
def set_location(node)
-
set_start_location(node)
-
set_end_location(node)
-
end
-
-
1
def set_start_location(node)
-
node.start_line = @start_line
-
node.start_column = @start_column
-
end
-
-
1
def set_end_location(node)
-
node.end_line = @end_line
-
node.end_column = @end_column
-
end
-
end
-
end
-
-
# frozen_string_literal: true
-
1
module Psych
-
# The version of Psych you are using
-
1
VERSION = '3.1.0' unless defined?(::Psych::VERSION)
-
-
1
if RUBY_ENGINE == 'jruby'
-
DEFAULT_SNAKEYAML_VERSION = '1.23'.freeze
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'psych/visitors/visitor'
-
1
require 'psych/visitors/to_ruby'
-
1
require 'psych/visitors/emitter'
-
1
require 'psych/visitors/yaml_tree'
-
1
require 'psych/visitors/json_tree'
-
1
require 'psych/visitors/depth_first'
-
# frozen_string_literal: true
-
1
module Psych
-
1
module Visitors
-
1
class DepthFirst < Psych::Visitors::Visitor
-
1
def initialize block
-
@block = block
-
end
-
-
1
private
-
-
1
def nary o
-
o.children.each { |x| visit x }
-
@block.call o
-
end
-
1
alias :visit_Psych_Nodes_Stream :nary
-
1
alias :visit_Psych_Nodes_Document :nary
-
1
alias :visit_Psych_Nodes_Sequence :nary
-
1
alias :visit_Psych_Nodes_Mapping :nary
-
-
1
def terminal o
-
@block.call o
-
end
-
1
alias :visit_Psych_Nodes_Scalar :terminal
-
1
alias :visit_Psych_Nodes_Alias :terminal
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
1
module Visitors
-
1
class Emitter < Psych::Visitors::Visitor
-
1
def initialize io, options = {}
-
opts = [:indentation, :canonical, :line_width].find_all { |opt|
-
options.key?(opt)
-
}
-
-
if opts.empty?
-
@handler = Psych::Emitter.new io
-
else
-
du = Handler::DumperOptions.new
-
opts.each { |option| du.send :"#{option}=", options[option] }
-
@handler = Psych::Emitter.new io, du
-
end
-
end
-
-
1
def visit_Psych_Nodes_Stream o
-
@handler.start_stream o.encoding
-
o.children.each { |c| accept c }
-
@handler.end_stream
-
end
-
-
1
def visit_Psych_Nodes_Document o
-
@handler.start_document o.version, o.tag_directives, o.implicit
-
o.children.each { |c| accept c }
-
@handler.end_document o.implicit_end
-
end
-
-
1
def visit_Psych_Nodes_Scalar o
-
@handler.scalar o.value, o.anchor, o.tag, o.plain, o.quoted, o.style
-
end
-
-
1
def visit_Psych_Nodes_Sequence o
-
@handler.start_sequence o.anchor, o.tag, o.implicit, o.style
-
o.children.each { |c| accept c }
-
@handler.end_sequence
-
end
-
-
1
def visit_Psych_Nodes_Mapping o
-
@handler.start_mapping o.anchor, o.tag, o.implicit, o.style
-
o.children.each { |c| accept c }
-
@handler.end_mapping
-
end
-
-
1
def visit_Psych_Nodes_Alias o
-
@handler.alias o.anchor
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'psych/json/ruby_events'
-
-
1
module Psych
-
1
module Visitors
-
1
class JSONTree < YAMLTree
-
1
include Psych::JSON::RubyEvents
-
-
1
def self.create options = {}
-
emitter = Psych::JSON::TreeBuilder.new
-
class_loader = ClassLoader.new
-
ss = ScalarScanner.new class_loader
-
new(emitter, ss, options)
-
end
-
-
1
def accept target
-
if target.respond_to?(:encode_with)
-
dump_coder target
-
else
-
send(@dispatch_cache[target.class], target)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'psych/scalar_scanner'
-
1
require 'psych/class_loader'
-
1
require 'psych/exception'
-
-
1
unless defined?(Regexp::NOENCODING)
-
Regexp::NOENCODING = 32
-
end
-
-
1
module Psych
-
1
module Visitors
-
###
-
# This class walks a YAML AST, converting each node to Ruby
-
1
class ToRuby < Psych::Visitors::Visitor
-
1
def self.create
-
class_loader = ClassLoader.new
-
scanner = ScalarScanner.new class_loader
-
new(scanner, class_loader)
-
end
-
-
1
attr_reader :class_loader
-
-
1
def initialize ss, class_loader
-
super()
-
@st = {}
-
@ss = ss
-
@domain_types = Psych.domain_types
-
@class_loader = class_loader
-
end
-
-
1
def accept target
-
result = super
-
return result if @domain_types.empty? || !target.tag
-
-
key = target.tag.sub(/^[!\/]*/, '').sub(/(,\d+)\//, '\1:')
-
key = "tag:#{key}" unless key =~ /^(?:tag:|x-private)/
-
-
if @domain_types.key? key
-
value, block = @domain_types[key]
-
return block.call value, result
-
end
-
-
result
-
end
-
-
1
def deserialize o
-
if klass = resolve_class(Psych.load_tags[o.tag])
-
instance = klass.allocate
-
-
if instance.respond_to?(:init_with)
-
coder = Psych::Coder.new(o.tag)
-
coder.scalar = o.value
-
instance.init_with coder
-
end
-
-
return instance
-
end
-
-
return o.value if o.quoted
-
return @ss.tokenize(o.value) unless o.tag
-
-
case o.tag
-
when '!binary', 'tag:yaml.org,2002:binary'
-
o.value.unpack('m').first
-
when /^!(?:str|ruby\/string)(?::(.*))?$/, 'tag:yaml.org,2002:str'
-
klass = resolve_class($1)
-
if klass
-
klass.allocate.replace o.value
-
else
-
o.value
-
end
-
when '!ruby/object:BigDecimal'
-
require 'bigdecimal' unless defined? BigDecimal
-
class_loader.big_decimal._load o.value
-
when "!ruby/object:DateTime"
-
class_loader.date_time
-
require 'date' unless defined? DateTime
-
@ss.parse_time(o.value).to_datetime
-
when '!ruby/encoding'
-
::Encoding.find o.value
-
when "!ruby/object:Complex"
-
class_loader.complex
-
Complex(o.value)
-
when "!ruby/object:Rational"
-
class_loader.rational
-
Rational(o.value)
-
when "!ruby/class", "!ruby/module"
-
resolve_class o.value
-
when "tag:yaml.org,2002:float", "!float"
-
Float(@ss.tokenize(o.value))
-
when "!ruby/regexp"
-
klass = class_loader.regexp
-
o.value =~ /^\/(.*)\/([mixn]*)$/m
-
source = $1
-
options = 0
-
lang = nil
-
($2 || '').split('').each do |option|
-
case option
-
when 'x' then options |= Regexp::EXTENDED
-
when 'i' then options |= Regexp::IGNORECASE
-
when 'm' then options |= Regexp::MULTILINE
-
when 'n' then options |= Regexp::NOENCODING
-
else lang = option
-
end
-
end
-
klass.new(*[source, options, lang].compact)
-
when "!ruby/range"
-
klass = class_loader.range
-
args = o.value.split(/([.]{2,3})/, 2).map { |s|
-
accept Nodes::Scalar.new(s)
-
}
-
args.push(args.delete_at(1) == '...')
-
klass.new(*args)
-
when /^!ruby\/sym(bol)?:?(.*)?$/
-
class_loader.symbolize o.value
-
else
-
@ss.tokenize o.value
-
end
-
end
-
1
private :deserialize
-
-
1
def visit_Psych_Nodes_Scalar o
-
register o, deserialize(o)
-
end
-
-
1
def visit_Psych_Nodes_Sequence o
-
if klass = resolve_class(Psych.load_tags[o.tag])
-
instance = klass.allocate
-
-
if instance.respond_to?(:init_with)
-
coder = Psych::Coder.new(o.tag)
-
coder.seq = o.children.map { |c| accept c }
-
instance.init_with coder
-
end
-
-
return instance
-
end
-
-
case o.tag
-
when nil
-
register_empty(o)
-
when '!omap', 'tag:yaml.org,2002:omap'
-
map = register(o, Psych::Omap.new)
-
o.children.each { |a|
-
map[accept(a.children.first)] = accept a.children.last
-
}
-
map
-
when /^!(?:seq|ruby\/array):(.*)$/
-
klass = resolve_class($1)
-
list = register(o, klass.allocate)
-
o.children.each { |c| list.push accept c }
-
list
-
else
-
register_empty(o)
-
end
-
end
-
-
1
def visit_Psych_Nodes_Mapping o
-
if Psych.load_tags[o.tag]
-
return revive(resolve_class(Psych.load_tags[o.tag]), o)
-
end
-
return revive_hash(register(o, {}), o) unless o.tag
-
-
case o.tag
-
when /^!ruby\/struct:?(.*)?$/
-
klass = resolve_class($1) if $1
-
-
if klass
-
s = register(o, klass.allocate)
-
-
members = {}
-
struct_members = s.members.map { |x| class_loader.symbolize x }
-
o.children.each_slice(2) do |k,v|
-
member = accept(k)
-
value = accept(v)
-
if struct_members.include?(class_loader.symbolize(member))
-
s.send("#{member}=", value)
-
else
-
members[member.to_s.sub(/^@/, '')] = value
-
end
-
end
-
init_with(s, members, o)
-
else
-
klass = class_loader.struct
-
members = o.children.map { |c| accept c }
-
h = Hash[*members]
-
s = klass.new(*h.map { |k,v|
-
class_loader.symbolize k
-
}).new(*h.map { |k,v| v })
-
register(o, s)
-
s
-
end
-
-
when /^!ruby\/object:?(.*)?$/
-
name = $1 || 'Object'
-
-
if name == 'Complex'
-
class_loader.complex
-
h = Hash[*o.children.map { |c| accept c }]
-
register o, Complex(h['real'], h['image'])
-
elsif name == 'Rational'
-
class_loader.rational
-
h = Hash[*o.children.map { |c| accept c }]
-
register o, Rational(h['numerator'], h['denominator'])
-
elsif name == 'Hash'
-
revive_hash(register(o, {}), o)
-
else
-
obj = revive((resolve_class(name) || class_loader.object), o)
-
obj
-
end
-
-
when /^!(?:str|ruby\/string)(?::(.*))?$/, 'tag:yaml.org,2002:str'
-
klass = resolve_class($1)
-
members = {}
-
string = nil
-
-
o.children.each_slice(2) do |k,v|
-
key = accept k
-
value = accept v
-
-
if key == 'str'
-
if klass
-
string = klass.allocate.replace value
-
else
-
string = value
-
end
-
register(o, string)
-
else
-
members[key] = value
-
end
-
end
-
init_with(string, members.map { |k,v| [k.to_s.sub(/^@/, ''),v] }, o)
-
when /^!ruby\/array:(.*)$/
-
klass = resolve_class($1)
-
list = register(o, klass.allocate)
-
-
members = Hash[o.children.map { |c| accept c }.each_slice(2).to_a]
-
list.replace members['internal']
-
-
members['ivars'].each do |ivar, v|
-
list.instance_variable_set ivar, v
-
end
-
list
-
-
when '!ruby/range'
-
klass = class_loader.range
-
h = Hash[*o.children.map { |c| accept c }]
-
register o, klass.new(h['begin'], h['end'], h['excl'])
-
-
when /^!ruby\/exception:?(.*)?$/
-
h = Hash[*o.children.map { |c| accept c }]
-
-
e = build_exception((resolve_class($1) || class_loader.exception),
-
h.delete('message'))
-
-
e.set_backtrace h.delete('backtrace') if h.key? 'backtrace'
-
init_with(e, h, o)
-
-
when '!set', 'tag:yaml.org,2002:set'
-
set = class_loader.psych_set.new
-
@st[o.anchor] = set if o.anchor
-
o.children.each_slice(2) do |k,v|
-
set[accept(k)] = accept(v)
-
end
-
set
-
-
when /^!ruby\/hash-with-ivars(?::(.*))?$/
-
hash = $1 ? resolve_class($1).allocate : {}
-
register o, hash
-
o.children.each_slice(2) do |key, value|
-
case key.value
-
when 'elements'
-
revive_hash hash, value
-
when 'ivars'
-
value.children.each_slice(2) do |k,v|
-
hash.instance_variable_set accept(k), accept(v)
-
end
-
end
-
end
-
hash
-
-
when /^!map:(.*)$/, /^!ruby\/hash:(.*)$/
-
revive_hash register(o, resolve_class($1).allocate), o
-
-
when '!omap', 'tag:yaml.org,2002:omap'
-
map = register(o, class_loader.psych_omap.new)
-
o.children.each_slice(2) do |l,r|
-
map[accept(l)] = accept r
-
end
-
map
-
-
when /^!ruby\/marshalable:(.*)$/
-
name = $1
-
klass = resolve_class(name)
-
obj = register(o, klass.allocate)
-
-
if obj.respond_to?(:init_with)
-
init_with(obj, revive_hash({}, o), o)
-
elsif obj.respond_to?(:marshal_load)
-
marshal_data = o.children.map(&method(:accept))
-
obj.marshal_load(marshal_data)
-
obj
-
else
-
raise ArgumentError, "Cannot deserialize #{name}"
-
end
-
-
else
-
revive_hash(register(o, {}), o)
-
end
-
end
-
-
1
def visit_Psych_Nodes_Document o
-
accept o.root
-
end
-
-
1
def visit_Psych_Nodes_Stream o
-
o.children.map { |c| accept c }
-
end
-
-
1
def visit_Psych_Nodes_Alias o
-
@st.fetch(o.anchor) { raise BadAlias, "Unknown alias: #{o.anchor}" }
-
end
-
-
1
private
-
1
def register node, object
-
@st[node.anchor] = object if node.anchor
-
object
-
end
-
-
1
def register_empty object
-
list = register(object, [])
-
object.children.each { |c| list.push accept c }
-
list
-
end
-
-
1
SHOVEL = '<<'
-
1
def revive_hash hash, o
-
o.children.each_slice(2) { |k,v|
-
key = deduplicate(accept(k))
-
val = accept(v)
-
-
if key == SHOVEL && k.tag != "tag:yaml.org,2002:str"
-
case v
-
when Nodes::Alias, Nodes::Mapping
-
begin
-
hash.merge! val
-
rescue TypeError
-
hash[key] = val
-
end
-
when Nodes::Sequence
-
begin
-
h = {}
-
val.reverse_each do |value|
-
h.merge! value
-
end
-
hash.merge! h
-
rescue TypeError
-
hash[key] = val
-
end
-
else
-
hash[key] = val
-
end
-
else
-
hash[key] = val
-
end
-
-
}
-
hash
-
end
-
-
1
if RUBY_VERSION < '2.7'
-
def deduplicate key
-
if key.is_a?(String)
-
-(key.untaint)
-
else
-
key
-
end
-
end
-
else
-
1
def deduplicate key
-
if key.is_a?(String)
-
-key
-
else
-
key
-
end
-
end
-
end
-
-
1
def merge_key hash, key, val
-
end
-
-
1
def revive klass, node
-
s = register(node, klass.allocate)
-
init_with(s, revive_hash({}, node), node)
-
end
-
-
1
def init_with o, h, node
-
c = Psych::Coder.new(node.tag)
-
c.map = h
-
-
if o.respond_to?(:init_with)
-
o.init_with c
-
else
-
h.each { |k,v| o.instance_variable_set(:"@#{k}", v) }
-
end
-
o
-
end
-
-
# Convert +klassname+ to a Class
-
1
def resolve_class klassname
-
class_loader.load klassname
-
end
-
end
-
-
1
class NoAliasRuby < ToRuby
-
1
def visit_Psych_Nodes_Alias o
-
raise BadAlias, "Unknown alias: #{o.anchor}"
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
module Psych
-
1
module Visitors
-
1
class Visitor
-
1
def accept target
-
visit target
-
end
-
-
1
private
-
-
1
DISPATCH = Hash.new do |hash, klass|
-
hash[klass] = "visit_#{klass.name.gsub('::', '_')}"
-
end
-
-
1
def visit target
-
send DISPATCH[target.class], target
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
1
require 'psych/tree_builder'
-
1
require 'psych/scalar_scanner'
-
1
require 'psych/class_loader'
-
-
1
module Psych
-
1
module Visitors
-
###
-
# YAMLTree builds a YAML ast given a Ruby object. For example:
-
#
-
# builder = Psych::Visitors::YAMLTree.new
-
# builder << { :foo => 'bar' }
-
# builder.tree # => #<Psych::Nodes::Stream .. }
-
#
-
1
class YAMLTree < Psych::Visitors::Visitor
-
1
class Registrar # :nodoc:
-
1
def initialize
-
@obj_to_id = {}
-
@obj_to_node = {}
-
@targets = []
-
@counter = 0
-
end
-
-
1
def register target, node
-
return unless target.respond_to? :object_id
-
@targets << target
-
@obj_to_node[target.object_id] = node
-
end
-
-
1
def key? target
-
@obj_to_node.key? target.object_id
-
rescue NoMethodError
-
false
-
end
-
-
1
def id_for target
-
@obj_to_id[target.object_id] ||= (@counter += 1)
-
end
-
-
1
def node_for target
-
@obj_to_node[target.object_id]
-
end
-
end
-
-
1
attr_reader :started, :finished
-
1
alias :finished? :finished
-
1
alias :started? :started
-
-
1
def self.create options = {}, emitter = nil
-
emitter ||= TreeBuilder.new
-
class_loader = ClassLoader.new
-
ss = ScalarScanner.new class_loader
-
new(emitter, ss, options)
-
end
-
-
1
def initialize emitter, ss, options
-
super()
-
@started = false
-
@finished = false
-
@emitter = emitter
-
@st = Registrar.new
-
@ss = ss
-
@options = options
-
@line_width = options[:line_width]
-
if @line_width && @line_width < 0
-
if @line_width == -1
-
# Treat -1 as unlimited line-width, same as libyaml does.
-
@line_width = nil
-
else
-
fail(ArgumentError, "Invalid line_width #{@line_width}, must be non-negative or -1 for unlimited.")
-
end
-
end
-
@coders = []
-
-
@dispatch_cache = Hash.new do |h,klass|
-
method = "visit_#{(klass.name || '').split('::').join('_')}"
-
-
method = respond_to?(method) ? method : h[klass.superclass]
-
-
raise(TypeError, "Can't dump #{target.class}") unless method
-
-
h[klass] = method
-
end
-
end
-
-
1
def start encoding = Nodes::Stream::UTF8
-
@emitter.start_stream(encoding).tap do
-
@started = true
-
end
-
end
-
-
1
def finish
-
@emitter.end_stream.tap do
-
@finished = true
-
end
-
end
-
-
1
def tree
-
finish unless finished?
-
@emitter.root
-
end
-
-
1
def push object
-
start unless started?
-
version = []
-
version = [1,1] if @options[:header]
-
-
case @options[:version]
-
when Array
-
version = @options[:version]
-
when String
-
version = @options[:version].split('.').map { |x| x.to_i }
-
else
-
version = [1,1]
-
end if @options.key? :version
-
-
@emitter.start_document version, [], false
-
accept object
-
@emitter.end_document !@emitter.streaming?
-
end
-
1
alias :<< :push
-
-
1
def accept target
-
# return any aliases we find
-
if @st.key? target
-
oid = @st.id_for target
-
node = @st.node_for target
-
anchor = oid.to_s
-
node.anchor = anchor
-
return @emitter.alias anchor
-
end
-
-
if target.respond_to?(:encode_with)
-
dump_coder target
-
else
-
send(@dispatch_cache[target.class], target)
-
end
-
end
-
-
1
def visit_Psych_Omap o
-
seq = @emitter.start_sequence(nil, 'tag:yaml.org,2002:omap', false, Nodes::Sequence::BLOCK)
-
register(o, seq)
-
-
o.each { |k,v| visit_Hash k => v }
-
@emitter.end_sequence
-
end
-
-
1
def visit_Encoding o
-
tag = "!ruby/encoding"
-
@emitter.scalar o.name, nil, tag, false, false, Nodes::Scalar::ANY
-
end
-
-
1
def visit_Object o
-
tag = Psych.dump_tags[o.class]
-
unless tag
-
klass = o.class == Object ? nil : o.class.name
-
tag = ['!ruby/object', klass].compact.join(':')
-
end
-
-
map = @emitter.start_mapping(nil, tag, false, Nodes::Mapping::BLOCK)
-
register(o, map)
-
-
dump_ivars o
-
@emitter.end_mapping
-
end
-
-
1
alias :visit_Delegator :visit_Object
-
-
1
def visit_Struct o
-
tag = ['!ruby/struct', o.class.name].compact.join(':')
-
-
register o, @emitter.start_mapping(nil, tag, false, Nodes::Mapping::BLOCK)
-
o.members.each do |member|
-
@emitter.scalar member.to_s, nil, nil, true, false, Nodes::Scalar::ANY
-
accept o[member]
-
end
-
-
dump_ivars o
-
-
@emitter.end_mapping
-
end
-
-
1
def visit_Exception o
-
dump_exception o, private_iv_get(o, 'mesg')
-
end
-
-
1
def visit_NameError o
-
dump_exception o, o.message.to_s
-
end
-
-
1
def visit_Regexp o
-
register o, @emitter.scalar(o.inspect, nil, '!ruby/regexp', false, false, Nodes::Scalar::ANY)
-
end
-
-
1
def visit_DateTime o
-
formatted = if o.offset.zero?
-
o.strftime("%Y-%m-%d %H:%M:%S.%9N Z".freeze)
-
else
-
o.strftime("%Y-%m-%d %H:%M:%S.%9N %:z".freeze)
-
end
-
tag = '!ruby/object:DateTime'
-
register o, @emitter.scalar(formatted, nil, tag, false, false, Nodes::Scalar::ANY)
-
end
-
-
1
def visit_Time o
-
formatted = format_time o
-
register o, @emitter.scalar(formatted, nil, nil, true, false, Nodes::Scalar::ANY)
-
end
-
-
1
def visit_Rational o
-
register o, @emitter.start_mapping(nil, '!ruby/object:Rational', false, Nodes::Mapping::BLOCK)
-
-
[
-
'denominator', o.denominator.to_s,
-
'numerator', o.numerator.to_s
-
].each do |m|
-
@emitter.scalar m, nil, nil, true, false, Nodes::Scalar::ANY
-
end
-
-
@emitter.end_mapping
-
end
-
-
1
def visit_Complex o
-
register o, @emitter.start_mapping(nil, '!ruby/object:Complex', false, Nodes::Mapping::BLOCK)
-
-
['real', o.real.to_s, 'image', o.imag.to_s].each do |m|
-
@emitter.scalar m, nil, nil, true, false, Nodes::Scalar::ANY
-
end
-
-
@emitter.end_mapping
-
end
-
-
1
def visit_Integer o
-
@emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
-
end
-
1
alias :visit_TrueClass :visit_Integer
-
1
alias :visit_FalseClass :visit_Integer
-
1
alias :visit_Date :visit_Integer
-
-
1
def visit_Float o
-
if o.nan?
-
@emitter.scalar '.nan', nil, nil, true, false, Nodes::Scalar::ANY
-
elsif o.infinite?
-
@emitter.scalar((o.infinite? > 0 ? '.inf' : '-.inf'),
-
nil, nil, true, false, Nodes::Scalar::ANY)
-
else
-
@emitter.scalar o.to_s, nil, nil, true, false, Nodes::Scalar::ANY
-
end
-
end
-
-
1
def visit_BigDecimal o
-
@emitter.scalar o._dump, nil, '!ruby/object:BigDecimal', false, false, Nodes::Scalar::ANY
-
end
-
-
1
def visit_String o
-
plain = true
-
quote = true
-
style = Nodes::Scalar::PLAIN
-
tag = nil
-
-
if binary?(o)
-
o = [o].pack('m0')
-
tag = '!binary' # FIXME: change to below when syck is removed
-
#tag = 'tag:yaml.org,2002:binary'
-
style = Nodes::Scalar::LITERAL
-
plain = false
-
quote = false
-
elsif o =~ /\n(?!\Z)/ # match \n except blank line at the end of string
-
style = Nodes::Scalar::LITERAL
-
elsif o == '<<'
-
style = Nodes::Scalar::SINGLE_QUOTED
-
tag = 'tag:yaml.org,2002:str'
-
plain = false
-
quote = false
-
elsif @line_width && o.length > @line_width
-
style = Nodes::Scalar::FOLDED
-
elsif o =~ /^[^[:word:]][^"]*$/
-
style = Nodes::Scalar::DOUBLE_QUOTED
-
elsif not String === @ss.tokenize(o) or /\A0[0-7]*[89]/ =~ o
-
style = Nodes::Scalar::SINGLE_QUOTED
-
end
-
-
is_primitive = o.class == ::String
-
ivars = is_primitive ? [] : o.instance_variables
-
-
if ivars.empty?
-
unless is_primitive
-
tag = "!ruby/string:#{o.class}"
-
plain = false
-
quote = false
-
end
-
@emitter.scalar o, nil, tag, plain, quote, style
-
else
-
maptag = '!ruby/string'.dup
-
maptag << ":#{o.class}" unless o.class == ::String
-
-
register o, @emitter.start_mapping(nil, maptag, false, Nodes::Mapping::BLOCK)
-
@emitter.scalar 'str', nil, nil, true, false, Nodes::Scalar::ANY
-
@emitter.scalar o, nil, tag, plain, quote, style
-
-
dump_ivars o
-
-
@emitter.end_mapping
-
end
-
end
-
-
1
def visit_Module o
-
raise TypeError, "can't dump anonymous module: #{o}" unless o.name
-
register o, @emitter.scalar(o.name, nil, '!ruby/module', false, false, Nodes::Scalar::SINGLE_QUOTED)
-
end
-
-
1
def visit_Class o
-
raise TypeError, "can't dump anonymous class: #{o}" unless o.name
-
register o, @emitter.scalar(o.name, nil, '!ruby/class', false, false, Nodes::Scalar::SINGLE_QUOTED)
-
end
-
-
1
def visit_Range o
-
register o, @emitter.start_mapping(nil, '!ruby/range', false, Nodes::Mapping::BLOCK)
-
['begin', o.begin, 'end', o.end, 'excl', o.exclude_end?].each do |m|
-
accept m
-
end
-
@emitter.end_mapping
-
end
-
-
1
def visit_Hash o
-
if o.class == ::Hash
-
register(o, @emitter.start_mapping(nil, nil, true, Psych::Nodes::Mapping::BLOCK))
-
o.each do |k,v|
-
accept k
-
accept v
-
end
-
@emitter.end_mapping
-
else
-
visit_hash_subclass o
-
end
-
end
-
-
1
def visit_Psych_Set o
-
register(o, @emitter.start_mapping(nil, '!set', false, Psych::Nodes::Mapping::BLOCK))
-
-
o.each do |k,v|
-
accept k
-
accept v
-
end
-
-
@emitter.end_mapping
-
end
-
-
1
def visit_Array o
-
if o.class == ::Array
-
visit_Enumerator o
-
else
-
visit_array_subclass o
-
end
-
end
-
-
1
def visit_Enumerator o
-
register o, @emitter.start_sequence(nil, nil, true, Nodes::Sequence::BLOCK)
-
o.each { |c| accept c }
-
@emitter.end_sequence
-
end
-
-
1
def visit_NilClass o
-
@emitter.scalar('', nil, 'tag:yaml.org,2002:null', true, false, Nodes::Scalar::ANY)
-
end
-
-
1
def visit_Symbol o
-
if o.empty?
-
@emitter.scalar "", nil, '!ruby/symbol', false, false, Nodes::Scalar::ANY
-
else
-
@emitter.scalar ":#{o}", nil, nil, true, false, Nodes::Scalar::ANY
-
end
-
end
-
-
1
def visit_BasicObject o
-
tag = Psych.dump_tags[o.class]
-
tag ||= "!ruby/marshalable:#{o.class.name}"
-
-
map = @emitter.start_mapping(nil, tag, false, Nodes::Mapping::BLOCK)
-
register(o, map)
-
-
o.marshal_dump.each(&method(:accept))
-
-
@emitter.end_mapping
-
end
-
-
1
private
-
-
1
def binary? string
-
string.encoding == Encoding::ASCII_8BIT && !string.ascii_only?
-
end
-
-
1
def visit_array_subclass o
-
tag = "!ruby/array:#{o.class}"
-
ivars = o.instance_variables
-
if ivars.empty?
-
node = @emitter.start_sequence(nil, tag, false, Nodes::Sequence::BLOCK)
-
register o, node
-
o.each { |c| accept c }
-
@emitter.end_sequence
-
else
-
node = @emitter.start_mapping(nil, tag, false, Nodes::Sequence::BLOCK)
-
register o, node
-
-
# Dump the internal list
-
accept 'internal'
-
@emitter.start_sequence(nil, nil, true, Nodes::Sequence::BLOCK)
-
o.each { |c| accept c }
-
@emitter.end_sequence
-
-
# Dump the ivars
-
accept 'ivars'
-
@emitter.start_mapping(nil, nil, true, Nodes::Sequence::BLOCK)
-
ivars.each do |ivar|
-
accept ivar
-
accept o.instance_variable_get ivar
-
end
-
@emitter.end_mapping
-
-
@emitter.end_mapping
-
end
-
end
-
-
1
def visit_hash_subclass o
-
ivars = o.instance_variables
-
if ivars.any?
-
tag = "!ruby/hash-with-ivars:#{o.class}"
-
node = @emitter.start_mapping(nil, tag, false, Psych::Nodes::Mapping::BLOCK)
-
register(o, node)
-
-
# Dump the ivars
-
accept 'ivars'
-
@emitter.start_mapping nil, nil, true, Nodes::Mapping::BLOCK
-
o.instance_variables.each do |ivar|
-
accept ivar
-
accept o.instance_variable_get ivar
-
end
-
@emitter.end_mapping
-
-
# Dump the elements
-
accept 'elements'
-
@emitter.start_mapping nil, nil, true, Nodes::Mapping::BLOCK
-
o.each do |k,v|
-
accept k
-
accept v
-
end
-
@emitter.end_mapping
-
-
@emitter.end_mapping
-
else
-
tag = "!ruby/hash:#{o.class}"
-
node = @emitter.start_mapping(nil, tag, false, Psych::Nodes::Mapping::BLOCK)
-
register(o, node)
-
o.each do |k,v|
-
accept k
-
accept v
-
end
-
@emitter.end_mapping
-
end
-
end
-
-
1
def dump_list o
-
end
-
-
1
def dump_exception o, msg
-
tag = ['!ruby/exception', o.class.name].join ':'
-
-
@emitter.start_mapping nil, tag, false, Nodes::Mapping::BLOCK
-
-
if msg
-
@emitter.scalar 'message', nil, nil, true, false, Nodes::Scalar::ANY
-
accept msg
-
end
-
-
@emitter.scalar 'backtrace', nil, nil, true, false, Nodes::Scalar::ANY
-
accept o.backtrace
-
-
dump_ivars o
-
-
@emitter.end_mapping
-
end
-
-
1
def format_time time
-
if time.utc?
-
time.strftime("%Y-%m-%d %H:%M:%S.%9N Z")
-
else
-
time.strftime("%Y-%m-%d %H:%M:%S.%9N %:z")
-
end
-
end
-
-
1
def register target, yaml_obj
-
@st.register target, yaml_obj
-
yaml_obj
-
end
-
-
1
def dump_coder o
-
@coders << o
-
tag = Psych.dump_tags[o.class]
-
unless tag
-
klass = o.class == Object ? nil : o.class.name
-
tag = ['!ruby/object', klass].compact.join(':')
-
end
-
-
c = Psych::Coder.new(tag)
-
o.encode_with(c)
-
emit_coder c, o
-
end
-
-
1
def emit_coder c, o
-
case c.type
-
when :scalar
-
@emitter.scalar c.scalar, nil, c.tag, c.tag.nil?, false, Nodes::Scalar::ANY
-
when :seq
-
@emitter.start_sequence nil, c.tag, c.tag.nil?, Nodes::Sequence::BLOCK
-
c.seq.each do |thing|
-
accept thing
-
end
-
@emitter.end_sequence
-
when :map
-
register o, @emitter.start_mapping(nil, c.tag, c.implicit, c.style)
-
c.map.each do |k,v|
-
accept k
-
accept v
-
end
-
@emitter.end_mapping
-
when :object
-
accept c.object
-
end
-
end
-
-
1
def dump_ivars target
-
target.instance_variables.each do |iv|
-
@emitter.scalar("#{iv.to_s.sub(/^@/, '')}", nil, nil, true, false, Nodes::Scalar::ANY)
-
accept target.instance_variable_get(iv)
-
end
-
end
-
end
-
end
-
end
-
# -*- coding: us-ascii -*-
-
# frozen_string_literal: true
-
-
# == Secure random number generator interface.
-
#
-
# This library is an interface to secure random number generators which are
-
# suitable for generating session keys in HTTP cookies, etc.
-
#
-
# You can use this library in your application by requiring it:
-
#
-
# require 'securerandom'
-
#
-
# It supports the following secure random number generators:
-
#
-
# * openssl
-
# * /dev/urandom
-
# * Win32
-
#
-
# SecureRandom is extended by the Random::Formatter module which
-
# defines the following methods:
-
#
-
# * alphanumeric
-
# * base64
-
# * choose
-
# * gen_random
-
# * hex
-
# * rand
-
# * random_bytes
-
# * random_number
-
# * urlsafe_base64
-
# * uuid
-
#
-
# These methods are usable as class methods of SecureRandom such as
-
# `SecureRandom.hex`.
-
#
-
# === Examples
-
#
-
# Generate random hexadecimal strings:
-
#
-
# require 'securerandom'
-
#
-
# SecureRandom.hex(10) #=> "52750b30ffbc7de3b362"
-
# SecureRandom.hex(10) #=> "92b15d6c8dc4beb5f559"
-
# SecureRandom.hex(13) #=> "39b290146bea6ce975c37cfc23"
-
#
-
# Generate random base64 strings:
-
#
-
# SecureRandom.base64(10) #=> "EcmTPZwWRAozdA=="
-
# SecureRandom.base64(10) #=> "KO1nIU+p9DKxGg=="
-
# SecureRandom.base64(12) #=> "7kJSM/MzBJI+75j8"
-
#
-
# Generate random binary strings:
-
#
-
# SecureRandom.random_bytes(10) #=> "\016\t{\370g\310pbr\301"
-
# SecureRandom.random_bytes(10) #=> "\323U\030TO\234\357\020\a\337"
-
#
-
# Generate alphanumeric strings:
-
#
-
# SecureRandom.alphanumeric(10) #=> "S8baxMJnPl"
-
# SecureRandom.alphanumeric(10) #=> "aOxAg8BAJe"
-
#
-
# Generate UUIDs:
-
#
-
# SecureRandom.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594"
-
# SecureRandom.uuid #=> "bad85eb9-0713-4da7-8d36-07a8e4b00eab"
-
#
-
-
1
module SecureRandom
-
1
@rng_chooser = Mutex.new # :nodoc:
-
-
1
class << self
-
1
def bytes(n)
-
return gen_random(n)
-
end
-
-
1
def gen_random(n)
-
1
ret = Random.urandom(1)
-
1
if ret.nil?
-
begin
-
require 'openssl'
-
rescue NoMethodError
-
raise NotImplementedError, "No random device"
-
else
-
@rng_chooser.synchronize do
-
class << self
-
remove_method :gen_random
-
alias gen_random gen_random_openssl
-
public :gen_random
-
end
-
end
-
return gen_random(n)
-
end
-
else
-
1
@rng_chooser.synchronize do
-
1
class << self
-
1
remove_method :gen_random
-
1
alias gen_random gen_random_urandom
-
1
public :gen_random
-
end
-
end
-
1
return gen_random(n)
-
end
-
end
-
-
1
private
-
-
1
def gen_random_openssl(n)
-
@pid = 0 unless defined?(@pid)
-
pid = $$
-
unless @pid == pid
-
now = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
-
OpenSSL::Random.random_add([now, @pid, pid].join(""), 0.0)
-
seed = Random.urandom(16)
-
if (seed)
-
OpenSSL::Random.random_add(seed, 16)
-
end
-
@pid = pid
-
end
-
return OpenSSL::Random.random_bytes(n)
-
end
-
-
1
def gen_random_urandom(n)
-
1
ret = Random.urandom(n)
-
1
unless ret
-
raise NotImplementedError, "No random device"
-
end
-
1
unless ret.length == n
-
raise NotImplementedError, "Unexpected partial read from random device: only #{ret.length} for #{n} bytes"
-
end
-
1
ret
-
end
-
end
-
end
-
-
1
module Random::Formatter
-
-
# SecureRandom.random_bytes generates a random binary string.
-
#
-
# The argument _n_ specifies the length of the result string.
-
#
-
# If _n_ is not specified or is nil, 16 is assumed.
-
# It may be larger in future.
-
#
-
# The result may contain any byte: "\x00" - "\xff".
-
#
-
# require 'securerandom'
-
#
-
# SecureRandom.random_bytes #=> "\xD8\\\xE0\xF4\r\xB2\xFC*WM\xFF\x83\x18\xF45\xB6"
-
# SecureRandom.random_bytes #=> "m\xDC\xFC/\a\x00Uf\xB2\xB2P\xBD\xFF6S\x97"
-
#
-
# If a secure random number generator is not available,
-
# +NotImplementedError+ is raised.
-
1
def random_bytes(n=nil)
-
1
n = n ? n.to_int : 16
-
1
gen_random(n)
-
end
-
-
# SecureRandom.hex generates a random hexadecimal string.
-
#
-
# The argument _n_ specifies the length, in bytes, of the random number to be generated.
-
# The length of the resulting hexadecimal string is twice of _n_.
-
#
-
# If _n_ is not specified or is nil, 16 is assumed.
-
# It may be larger in the future.
-
#
-
# The result may contain 0-9 and a-f.
-
#
-
# require 'securerandom'
-
#
-
# SecureRandom.hex #=> "eb693ec8252cd630102fd0d0fb7c3485"
-
# SecureRandom.hex #=> "91dc3bfb4de5b11d029d376634589b61"
-
#
-
# If a secure random number generator is not available,
-
# +NotImplementedError+ is raised.
-
1
def hex(n=nil)
-
1
random_bytes(n).unpack("H*")[0]
-
end
-
-
# SecureRandom.base64 generates a random base64 string.
-
#
-
# The argument _n_ specifies the length, in bytes, of the random number
-
# to be generated. The length of the result string is about 4/3 of _n_.
-
#
-
# If _n_ is not specified or is nil, 16 is assumed.
-
# It may be larger in the future.
-
#
-
# The result may contain A-Z, a-z, 0-9, "+", "/" and "=".
-
#
-
# require 'securerandom'
-
#
-
# SecureRandom.base64 #=> "/2BuBuLf3+WfSKyQbRcc/A=="
-
# SecureRandom.base64 #=> "6BbW0pxO0YENxn38HMUbcQ=="
-
#
-
# If a secure random number generator is not available,
-
# +NotImplementedError+ is raised.
-
#
-
# See RFC 3548 for the definition of base64.
-
1
def base64(n=nil)
-
[random_bytes(n)].pack("m0")
-
end
-
-
# SecureRandom.urlsafe_base64 generates a random URL-safe base64 string.
-
#
-
# The argument _n_ specifies the length, in bytes, of the random number
-
# to be generated. The length of the result string is about 4/3 of _n_.
-
#
-
# If _n_ is not specified or is nil, 16 is assumed.
-
# It may be larger in the future.
-
#
-
# The boolean argument _padding_ specifies the padding.
-
# If it is false or nil, padding is not generated.
-
# Otherwise padding is generated.
-
# By default, padding is not generated because "=" may be used as a URL delimiter.
-
#
-
# The result may contain A-Z, a-z, 0-9, "-" and "_".
-
# "=" is also used if _padding_ is true.
-
#
-
# require 'securerandom'
-
#
-
# SecureRandom.urlsafe_base64 #=> "b4GOKm4pOYU_-BOXcrUGDg"
-
# SecureRandom.urlsafe_base64 #=> "UZLdOkzop70Ddx-IJR0ABg"
-
#
-
# SecureRandom.urlsafe_base64(nil, true) #=> "i0XQ-7gglIsHGV2_BNPrdQ=="
-
# SecureRandom.urlsafe_base64(nil, true) #=> "-M8rLhr7JEpJlqFGUMmOxg=="
-
#
-
# If a secure random number generator is not available,
-
# +NotImplementedError+ is raised.
-
#
-
# See RFC 3548 for the definition of URL-safe base64.
-
1
def urlsafe_base64(n=nil, padding=false)
-
s = [random_bytes(n)].pack("m0")
-
s.tr!("+/", "-_")
-
s.delete!("=") unless padding
-
s
-
end
-
-
# SecureRandom.uuid generates a random v4 UUID (Universally Unique IDentifier).
-
#
-
# require 'securerandom'
-
#
-
# SecureRandom.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594"
-
# SecureRandom.uuid #=> "bad85eb9-0713-4da7-8d36-07a8e4b00eab"
-
# SecureRandom.uuid #=> "62936e70-1815-439b-bf89-8492855a7e6b"
-
#
-
# The version 4 UUID is purely random (except the version).
-
# It doesn't contain meaningful information such as MAC addresses, timestamps, etc.
-
#
-
# The result contains 122 random bits (15.25 random bytes).
-
#
-
# See RFC 4122 for details of UUID.
-
#
-
1
def uuid
-
ary = random_bytes(16).unpack("NnnnnN")
-
ary[2] = (ary[2] & 0x0fff) | 0x4000
-
ary[3] = (ary[3] & 0x3fff) | 0x8000
-
"%08x-%04x-%04x-%04x-%04x%08x" % ary
-
end
-
-
1
private def gen_random(n)
-
self.bytes(n)
-
end
-
-
# SecureRandom.choose generates a string that randomly draws from a
-
# source array of characters.
-
#
-
# The argument _source_ specifies the array of characters from which
-
# to generate the string.
-
# The argument _n_ specifies the length, in characters, of the string to be
-
# generated.
-
#
-
# The result may contain whatever characters are in the source array.
-
#
-
# require 'securerandom'
-
#
-
# SecureRandom.choose([*'l'..'r'], 16) #=> "lmrqpoonmmlqlron"
-
# SecureRandom.choose([*'0'..'9'], 5) #=> "27309"
-
#
-
# If a secure random number generator is not available,
-
# +NotImplementedError+ is raised.
-
1
private def choose(source, n)
-
size = source.size
-
m = 1
-
limit = size
-
while limit * size <= 0x100000000
-
limit *= size
-
m += 1
-
end
-
result = ''.dup
-
while m <= n
-
rs = random_number(limit)
-
is = rs.digits(size)
-
(m-is.length).times { is << 0 }
-
result << source.values_at(*is).join('')
-
n -= m
-
end
-
if 0 < n
-
rs = random_number(limit)
-
is = rs.digits(size)
-
if is.length < n
-
(n-is.length).times { is << 0 }
-
else
-
is.pop while n < is.length
-
end
-
result.concat source.values_at(*is).join('')
-
end
-
result
-
end
-
-
1
ALPHANUMERIC = [*'A'..'Z', *'a'..'z', *'0'..'9']
-
# SecureRandom.alphanumeric generates a random alphanumeric string.
-
#
-
# The argument _n_ specifies the length, in characters, of the alphanumeric
-
# string to be generated.
-
#
-
# If _n_ is not specified or is nil, 16 is assumed.
-
# It may be larger in the future.
-
#
-
# The result may contain A-Z, a-z and 0-9.
-
#
-
# require 'securerandom'
-
#
-
# SecureRandom.alphanumeric #=> "2BuBuLf3WfSKyQbR"
-
# SecureRandom.alphanumeric(10) #=> "i6K93NdqiH"
-
#
-
# If a secure random number generator is not available,
-
# +NotImplementedError+ is raised.
-
1
def alphanumeric(n=nil)
-
n = 16 if n.nil?
-
choose(ALPHANUMERIC, n)
-
end
-
end
-
-
1
SecureRandom.extend(Random::Formatter)
-
# frozen-string-literal: true
-
##
-
# == Manipulates strings like the UNIX Bourne shell
-
#
-
# This module manipulates strings according to the word parsing rules
-
# of the UNIX Bourne shell.
-
#
-
# The shellwords() function was originally a port of shellwords.pl,
-
# but modified to conform to the Shell & Utilities volume of the IEEE
-
# Std 1003.1-2008, 2016 Edition [1].
-
#
-
# === Usage
-
#
-
# You can use Shellwords to parse a string into a Bourne shell friendly Array.
-
#
-
# require 'shellwords'
-
#
-
# argv = Shellwords.split('three blind "mice"')
-
# argv #=> ["three", "blind", "mice"]
-
#
-
# Once you've required Shellwords, you can use the #split alias
-
# String#shellsplit.
-
#
-
# argv = "see how they run".shellsplit
-
# argv #=> ["see", "how", "they", "run"]
-
#
-
# Be careful you don't leave a quote unmatched.
-
#
-
# argv = "they all ran after the farmer's wife".shellsplit
-
# #=> ArgumentError: Unmatched double quote: ...
-
#
-
# In this case, you might want to use Shellwords.escape, or its alias
-
# String#shellescape.
-
#
-
# This method will escape the String for you to safely use with a Bourne shell.
-
#
-
# argv = Shellwords.escape("special's.txt")
-
# argv #=> "special\\'s.txt"
-
# system("cat " + argv)
-
#
-
# Shellwords also comes with a core extension for Array, Array#shelljoin.
-
#
-
# argv = %w{ls -lta lib}
-
# system(argv.shelljoin)
-
#
-
# You can use this method to create an escaped string out of an array of tokens
-
# separated by a space. In this example we used the literal shortcut for
-
# Array.new.
-
#
-
# === Authors
-
# * Wakou Aoyama
-
# * Akinori MUSHA <knu@iDaemons.org>
-
#
-
# === Contact
-
# * Akinori MUSHA <knu@iDaemons.org> (current maintainer)
-
#
-
# === Resources
-
#
-
# 1: {IEEE Std 1003.1-2008, 2016 Edition, the Shell & Utilities volume}[http://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html]
-
-
1
module Shellwords
-
# Splits a string into an array of tokens in the same way the UNIX
-
# Bourne shell does.
-
#
-
# argv = Shellwords.split('here are "two words"')
-
# argv #=> ["here", "are", "two words"]
-
#
-
# Note, however, that this is not a command line parser. Shell
-
# metacharacters except for the single and double quotes and
-
# backslash are not treated as such.
-
#
-
# argv = Shellwords.split('ruby my_prog.rb | less')
-
# argv #=> ["ruby", "my_prog.rb", "|", "less"]
-
#
-
# String#shellsplit is a shortcut for this function.
-
#
-
# argv = 'here are "two words"'.shellsplit
-
# argv #=> ["here", "are", "two words"]
-
1
def shellsplit(line)
-
2
words = []
-
2
field = String.new
-
2
line.scan(/\G\s*(?>([^\s\\\'\"]+)|'([^\']*)'|"((?:[^\"\\]|\\.)*)"|(\\.?)|(\S))(\s|\z)?/m) do
-
|word, sq, dq, esc, garbage, sep|
-
4
raise ArgumentError, "Unmatched double quote: #{line.inspect}" if garbage
-
# 2.2.3 Double-Quotes:
-
#
-
# The <backslash> shall retain its special meaning as an
-
# escape character only when followed by one of the following
-
# characters when considered special:
-
#
-
# $ ` " \ <newline>
-
4
field << (word || sq || (dq && dq.gsub(/\\([$`"\\\n])/, '\\1')) || esc.gsub(/\\(.)/, '\\1'))
-
4
if sep
-
4
words << field
-
4
field = String.new
-
end
-
end
-
2
words
-
end
-
-
1
alias shellwords shellsplit
-
-
1
module_function :shellsplit, :shellwords
-
-
1
class << self
-
1
alias split shellsplit
-
end
-
-
# Escapes a string so that it can be safely used in a Bourne shell
-
# command line. +str+ can be a non-string object that responds to
-
# +to_s+.
-
#
-
# Note that a resulted string should be used unquoted and is not
-
# intended for use in double quotes nor in single quotes.
-
#
-
# argv = Shellwords.escape("It's better to give than to receive")
-
# argv #=> "It\\'s\\ better\\ to\\ give\\ than\\ to\\ receive"
-
#
-
# String#shellescape is a shorthand for this function.
-
#
-
# argv = "It's better to give than to receive".shellescape
-
# argv #=> "It\\'s\\ better\\ to\\ give\\ than\\ to\\ receive"
-
#
-
# # Search files in lib for method definitions
-
# pattern = "^[ \t]*def "
-
# open("| grep -Ern #{pattern.shellescape} lib") { |grep|
-
# grep.each_line { |line|
-
# file, lineno, matched_line = line.split(':', 3)
-
# # ...
-
# }
-
# }
-
#
-
# It is the caller's responsibility to encode the string in the right
-
# encoding for the shell environment where this string is used.
-
#
-
# Multibyte characters are treated as multibyte characters, not as bytes.
-
#
-
# Returns an empty quoted String if +str+ has a length of zero.
-
1
def shellescape(str)
-
str = str.to_s
-
-
# An empty argument will be skipped, so return empty quotes.
-
return "''".dup if str.empty?
-
-
str = str.dup
-
-
# Treat multibyte characters as is. It is the caller's responsibility
-
# to encode the string in the right encoding for the shell
-
# environment.
-
str.gsub!(/[^A-Za-z0-9_\-.,:+\/@\n]/, "\\\\\\&")
-
-
# A LF cannot be escaped with a backslash because a backslash + LF
-
# combo is regarded as a line continuation and simply ignored.
-
str.gsub!(/\n/, "'\n'")
-
-
return str
-
end
-
-
1
module_function :shellescape
-
-
1
class << self
-
1
alias escape shellescape
-
end
-
-
# Builds a command line string from an argument list, +array+.
-
#
-
# All elements are joined into a single string with fields separated by a
-
# space, where each element is escaped for the Bourne shell and stringified
-
# using +to_s+.
-
#
-
# ary = ["There's", "a", "time", "and", "place", "for", "everything"]
-
# argv = Shellwords.join(ary)
-
# argv #=> "There\\'s a time and place for everything"
-
#
-
# Array#shelljoin is a shortcut for this function.
-
#
-
# ary = ["Don't", "rock", "the", "boat"]
-
# argv = ary.shelljoin
-
# argv #=> "Don\\'t rock the boat"
-
#
-
# You can also mix non-string objects in the elements as allowed in Array#join.
-
#
-
# output = `#{['ps', '-p', $$].shelljoin}`
-
#
-
1
def shelljoin(array)
-
array.map { |arg| shellescape(arg) }.join(' ')
-
end
-
-
1
module_function :shelljoin
-
-
1
class << self
-
1
alias join shelljoin
-
end
-
end
-
-
1
class String
-
# call-seq:
-
# str.shellsplit => array
-
#
-
# Splits +str+ into an array of tokens in the same way the UNIX
-
# Bourne shell does.
-
#
-
# See Shellwords.shellsplit for details.
-
1
def shellsplit
-
Shellwords.split(self)
-
end
-
-
# call-seq:
-
# str.shellescape => string
-
#
-
# Escapes +str+ so that it can be safely used in a Bourne shell
-
# command line.
-
#
-
# See Shellwords.shellescape for details.
-
1
def shellescape
-
Shellwords.escape(self)
-
end
-
end
-
-
1
class Array
-
# call-seq:
-
# array.shelljoin => string
-
#
-
# Builds a command line string from an argument list +array+ joining
-
# all elements escaped for the Bourne shell and separated by a space.
-
#
-
# See Shellwords.shelljoin for details.
-
1
def shelljoin
-
Shellwords.join(self)
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require 'socket.so'
-
1
require 'io/wait'
-
-
1
class Addrinfo
-
# creates an Addrinfo object from the arguments.
-
#
-
# The arguments are interpreted as similar to self.
-
#
-
# Addrinfo.tcp("0.0.0.0", 4649).family_addrinfo("www.ruby-lang.org", 80)
-
# #=> #<Addrinfo: 221.186.184.68:80 TCP (www.ruby-lang.org:80)>
-
#
-
# Addrinfo.unix("/tmp/sock").family_addrinfo("/tmp/sock2")
-
# #=> #<Addrinfo: /tmp/sock2 SOCK_STREAM>
-
#
-
1
def family_addrinfo(*args)
-
if args.empty?
-
raise ArgumentError, "no address specified"
-
elsif Addrinfo === args.first
-
raise ArgumentError, "too many arguments" if args.length != 1
-
addrinfo = args.first
-
if (self.pfamily != addrinfo.pfamily) ||
-
(self.socktype != addrinfo.socktype)
-
raise ArgumentError, "Addrinfo type mismatch"
-
end
-
addrinfo
-
elsif self.ip?
-
raise ArgumentError, "IP address needs host and port but #{args.length} arguments given" if args.length != 2
-
host, port = args
-
Addrinfo.getaddrinfo(host, port, self.pfamily, self.socktype, self.protocol)[0]
-
elsif self.unix?
-
raise ArgumentError, "UNIX socket needs single path argument but #{args.length} arguments given" if args.length != 1
-
path, = args
-
Addrinfo.unix(path)
-
else
-
raise ArgumentError, "unexpected family"
-
end
-
end
-
-
# creates a new Socket connected to the address of +local_addrinfo+.
-
#
-
# If _local_addrinfo_ is nil, the address of the socket is not bound.
-
#
-
# The _timeout_ specify the seconds for timeout.
-
# Errno::ETIMEDOUT is raised when timeout occur.
-
#
-
# If a block is given the created socket is yielded for each address.
-
#
-
1
def connect_internal(local_addrinfo, timeout=nil) # :yields: socket
-
sock = Socket.new(self.pfamily, self.socktype, self.protocol)
-
begin
-
sock.ipv6only! if self.ipv6?
-
sock.bind local_addrinfo if local_addrinfo
-
if timeout
-
case sock.connect_nonblock(self, exception: false)
-
when 0 # success or EISCONN, other errors raise
-
break
-
when :wait_writable
-
sock.wait_writable(timeout) or
-
raise Errno::ETIMEDOUT, 'user specified timeout'
-
end while true
-
else
-
sock.connect(self)
-
end
-
rescue Exception
-
sock.close
-
raise
-
end
-
if block_given?
-
begin
-
yield sock
-
ensure
-
sock.close
-
end
-
else
-
sock
-
end
-
end
-
1
protected :connect_internal
-
-
# :call-seq:
-
# addrinfo.connect_from([local_addr_args], [opts]) {|socket| ... }
-
# addrinfo.connect_from([local_addr_args], [opts])
-
#
-
# creates a socket connected to the address of self.
-
#
-
# If one or more arguments given as _local_addr_args_,
-
# it is used as the local address of the socket.
-
# _local_addr_args_ is given for family_addrinfo to obtain actual address.
-
#
-
# If _local_addr_args_ is not given, the local address of the socket is not bound.
-
#
-
# The optional last argument _opts_ is options represented by a hash.
-
# _opts_ may have following options:
-
#
-
# [:timeout] specify the timeout in seconds.
-
#
-
# If a block is given, it is called with the socket and the value of the block is returned.
-
# The socket is returned otherwise.
-
#
-
# Addrinfo.tcp("www.ruby-lang.org", 80).connect_from("0.0.0.0", 4649) {|s|
-
# s.print "GET / HTTP/1.0\r\nHost: www.ruby-lang.org\r\n\r\n"
-
# puts s.read
-
# }
-
#
-
# # Addrinfo object can be taken for the argument.
-
# Addrinfo.tcp("www.ruby-lang.org", 80).connect_from(Addrinfo.tcp("0.0.0.0", 4649)) {|s|
-
# s.print "GET / HTTP/1.0\r\nHost: www.ruby-lang.org\r\n\r\n"
-
# puts s.read
-
# }
-
#
-
1
def connect_from(*args, timeout: nil, &block)
-
connect_internal(family_addrinfo(*args), timeout, &block)
-
end
-
-
# :call-seq:
-
# addrinfo.connect([opts]) {|socket| ... }
-
# addrinfo.connect([opts])
-
#
-
# creates a socket connected to the address of self.
-
#
-
# The optional argument _opts_ is options represented by a hash.
-
# _opts_ may have following options:
-
#
-
# [:timeout] specify the timeout in seconds.
-
#
-
# If a block is given, it is called with the socket and the value of the block is returned.
-
# The socket is returned otherwise.
-
#
-
# Addrinfo.tcp("www.ruby-lang.org", 80).connect {|s|
-
# s.print "GET / HTTP/1.0\r\nHost: www.ruby-lang.org\r\n\r\n"
-
# puts s.read
-
# }
-
#
-
1
def connect(timeout: nil, &block)
-
connect_internal(nil, timeout, &block)
-
end
-
-
# :call-seq:
-
# addrinfo.connect_to([remote_addr_args], [opts]) {|socket| ... }
-
# addrinfo.connect_to([remote_addr_args], [opts])
-
#
-
# creates a socket connected to _remote_addr_args_ and bound to self.
-
#
-
# The optional last argument _opts_ is options represented by a hash.
-
# _opts_ may have following options:
-
#
-
# [:timeout] specify the timeout in seconds.
-
#
-
# If a block is given, it is called with the socket and the value of the block is returned.
-
# The socket is returned otherwise.
-
#
-
# Addrinfo.tcp("0.0.0.0", 4649).connect_to("www.ruby-lang.org", 80) {|s|
-
# s.print "GET / HTTP/1.0\r\nHost: www.ruby-lang.org\r\n\r\n"
-
# puts s.read
-
# }
-
#
-
1
def connect_to(*args, timeout: nil, &block)
-
remote_addrinfo = family_addrinfo(*args)
-
remote_addrinfo.connect_internal(self, timeout, &block)
-
end
-
-
# creates a socket bound to self.
-
#
-
# If a block is given, it is called with the socket and the value of the block is returned.
-
# The socket is returned otherwise.
-
#
-
# Addrinfo.udp("0.0.0.0", 9981).bind {|s|
-
# s.local_address.connect {|s| s.send "hello", 0 }
-
# p s.recv(10) #=> "hello"
-
# }
-
#
-
1
def bind
-
sock = Socket.new(self.pfamily, self.socktype, self.protocol)
-
begin
-
sock.ipv6only! if self.ipv6?
-
sock.setsockopt(:SOCKET, :REUSEADDR, 1)
-
sock.bind(self)
-
rescue Exception
-
sock.close
-
raise
-
end
-
if block_given?
-
begin
-
yield sock
-
ensure
-
sock.close
-
end
-
else
-
sock
-
end
-
end
-
-
# creates a listening socket bound to self.
-
1
def listen(backlog=Socket::SOMAXCONN)
-
sock = Socket.new(self.pfamily, self.socktype, self.protocol)
-
begin
-
sock.ipv6only! if self.ipv6?
-
sock.setsockopt(:SOCKET, :REUSEADDR, 1)
-
sock.bind(self)
-
sock.listen(backlog)
-
rescue Exception
-
sock.close
-
raise
-
end
-
if block_given?
-
begin
-
yield sock
-
ensure
-
sock.close
-
end
-
else
-
sock
-
end
-
end
-
-
# iterates over the list of Addrinfo objects obtained by Addrinfo.getaddrinfo.
-
#
-
# Addrinfo.foreach(nil, 80) {|x| p x }
-
# #=> #<Addrinfo: 127.0.0.1:80 TCP (:80)>
-
# # #<Addrinfo: 127.0.0.1:80 UDP (:80)>
-
# # #<Addrinfo: [::1]:80 TCP (:80)>
-
# # #<Addrinfo: [::1]:80 UDP (:80)>
-
#
-
1
def self.foreach(nodename, service, family=nil, socktype=nil, protocol=nil, flags=nil, timeout: nil, &block)
-
Addrinfo.getaddrinfo(nodename, service, family, socktype, protocol, flags, timeout: timeout).each(&block)
-
end
-
end
-
-
1
class BasicSocket < IO
-
# Returns an address of the socket suitable for connect in the local machine.
-
#
-
# This method returns _self_.local_address, except following condition.
-
#
-
# - IPv4 unspecified address (0.0.0.0) is replaced by IPv4 loopback address (127.0.0.1).
-
# - IPv6 unspecified address (::) is replaced by IPv6 loopback address (::1).
-
#
-
# If the local address is not suitable for connect, SocketError is raised.
-
# IPv4 and IPv6 address which port is 0 is not suitable for connect.
-
# Unix domain socket which has no path is not suitable for connect.
-
#
-
# Addrinfo.tcp("0.0.0.0", 0).listen {|serv|
-
# p serv.connect_address #=> #<Addrinfo: 127.0.0.1:53660 TCP>
-
# serv.connect_address.connect {|c|
-
# s, _ = serv.accept
-
# p [c, s] #=> [#<Socket:fd 4>, #<Socket:fd 6>]
-
# }
-
# }
-
#
-
1
def connect_address
-
addr = local_address
-
afamily = addr.afamily
-
if afamily == Socket::AF_INET
-
raise SocketError, "unbound IPv4 socket" if addr.ip_port == 0
-
if addr.ip_address == "0.0.0.0"
-
addr = Addrinfo.new(["AF_INET", addr.ip_port, nil, "127.0.0.1"], addr.pfamily, addr.socktype, addr.protocol)
-
end
-
elsif defined?(Socket::AF_INET6) && afamily == Socket::AF_INET6
-
raise SocketError, "unbound IPv6 socket" if addr.ip_port == 0
-
if addr.ip_address == "::"
-
addr = Addrinfo.new(["AF_INET6", addr.ip_port, nil, "::1"], addr.pfamily, addr.socktype, addr.protocol)
-
elsif addr.ip_address == "0.0.0.0" # MacOS X 10.4 returns "a.b.c.d" for IPv4-mapped IPv6 address.
-
addr = Addrinfo.new(["AF_INET6", addr.ip_port, nil, "::1"], addr.pfamily, addr.socktype, addr.protocol)
-
elsif addr.ip_address == "::ffff:0.0.0.0" # MacOS X 10.6 returns "::ffff:a.b.c.d" for IPv4-mapped IPv6 address.
-
addr = Addrinfo.new(["AF_INET6", addr.ip_port, nil, "::1"], addr.pfamily, addr.socktype, addr.protocol)
-
end
-
elsif defined?(Socket::AF_UNIX) && afamily == Socket::AF_UNIX
-
raise SocketError, "unbound Unix socket" if addr.unix_path == ""
-
end
-
addr
-
end
-
-
# call-seq:
-
# basicsocket.sendmsg(mesg, flags=0, dest_sockaddr=nil, *controls) => numbytes_sent
-
#
-
# sendmsg sends a message using sendmsg(2) system call in blocking manner.
-
#
-
# _mesg_ is a string to send.
-
#
-
# _flags_ is bitwise OR of MSG_* constants such as Socket::MSG_OOB.
-
#
-
# _dest_sockaddr_ is a destination socket address for connection-less socket.
-
# It should be a sockaddr such as a result of Socket.sockaddr_in.
-
# An Addrinfo object can be used too.
-
#
-
# _controls_ is a list of ancillary data.
-
# The element of _controls_ should be Socket::AncillaryData or
-
# 3-elements array.
-
# The 3-element array should contains cmsg_level, cmsg_type and data.
-
#
-
# The return value, _numbytes_sent_ is an integer which is the number of bytes sent.
-
#
-
# sendmsg can be used to implement send_io as follows:
-
#
-
# # use Socket::AncillaryData.
-
# ancdata = Socket::AncillaryData.int(:UNIX, :SOCKET, :RIGHTS, io.fileno)
-
# sock.sendmsg("a", 0, nil, ancdata)
-
#
-
# # use 3-element array.
-
# ancdata = [:SOCKET, :RIGHTS, [io.fileno].pack("i!")]
-
# sock.sendmsg("\0", 0, nil, ancdata)
-
1
def sendmsg(mesg, flags = 0, dest_sockaddr = nil, *controls)
-
__sendmsg(mesg, flags, dest_sockaddr, controls)
-
end
-
-
# call-seq:
-
# basicsocket.sendmsg_nonblock(mesg, flags=0, dest_sockaddr=nil, *controls, opts={}) => numbytes_sent
-
#
-
# sendmsg_nonblock sends a message using sendmsg(2) system call in non-blocking manner.
-
#
-
# It is similar to BasicSocket#sendmsg
-
# but the non-blocking flag is set before the system call
-
# and it doesn't retry the system call.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that sendmsg_nonblock should not raise an IO::WaitWritable exception, but
-
# return the symbol +:wait_writable+ instead.
-
1
def sendmsg_nonblock(mesg, flags = 0, dest_sockaddr = nil, *controls,
-
exception: true)
-
__sendmsg_nonblock(mesg, flags, dest_sockaddr, controls, exception)
-
end
-
-
# call-seq:
-
# basicsocket.recv_nonblock(maxlen [, flags [, buf [, options ]]]) => mesg
-
#
-
# Receives up to _maxlen_ bytes from +socket+ using recvfrom(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# _flags_ is zero or more of the +MSG_+ options.
-
# The result, _mesg_, is the data received.
-
#
-
# When recvfrom(2) returns 0, Socket#recv_nonblock returns
-
# an empty string as data.
-
# The meaning depends on the socket: EOF on TCP, empty packet on UDP, etc.
-
#
-
# === Parameters
-
# * +maxlen+ - the number of bytes to receive from the socket
-
# * +flags+ - zero or more of the +MSG_+ options
-
# * +buf+ - destination String buffer
-
# * +options+ - keyword hash, supporting `exception: false`
-
#
-
# === Example
-
# serv = TCPServer.new("127.0.0.1", 0)
-
# af, port, host, addr = serv.addr
-
# c = TCPSocket.new(addr, port)
-
# s = serv.accept
-
# c.send "aaa", 0
-
# begin # emulate blocking recv.
-
# p s.recv_nonblock(10) #=> "aaa"
-
# rescue IO::WaitReadable
-
# IO.select([s])
-
# retry
-
# end
-
#
-
# Refer to Socket#recvfrom for the exceptions that may be thrown if the call
-
# to _recv_nonblock_ fails.
-
#
-
# BasicSocket#recv_nonblock may raise any error corresponding to recvfrom(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK or Errno::EAGAIN,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying recv_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that recv_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * Socket#recvfrom
-
1
def recv_nonblock(len, flag = 0, str = nil, exception: true)
-
__recv_nonblock(len, flag, str, exception)
-
end
-
-
# call-seq:
-
# basicsocket.recvmsg(maxmesglen=nil, flags=0, maxcontrollen=nil, opts={}) => [mesg, sender_addrinfo, rflags, *controls]
-
#
-
# recvmsg receives a message using recvmsg(2) system call in blocking manner.
-
#
-
# _maxmesglen_ is the maximum length of mesg to receive.
-
#
-
# _flags_ is bitwise OR of MSG_* constants such as Socket::MSG_PEEK.
-
#
-
# _maxcontrollen_ is the maximum length of controls (ancillary data) to receive.
-
#
-
# _opts_ is option hash.
-
# Currently :scm_rights=>bool is the only option.
-
#
-
# :scm_rights option specifies that application expects SCM_RIGHTS control message.
-
# If the value is nil or false, application don't expects SCM_RIGHTS control message.
-
# In this case, recvmsg closes the passed file descriptors immediately.
-
# This is the default behavior.
-
#
-
# If :scm_rights value is neither nil nor false, application expects SCM_RIGHTS control message.
-
# In this case, recvmsg creates IO objects for each file descriptors for
-
# Socket::AncillaryData#unix_rights method.
-
#
-
# The return value is 4-elements array.
-
#
-
# _mesg_ is a string of the received message.
-
#
-
# _sender_addrinfo_ is a sender socket address for connection-less socket.
-
# It is an Addrinfo object.
-
# For connection-oriented socket such as TCP, sender_addrinfo is platform dependent.
-
#
-
# _rflags_ is a flags on the received message which is bitwise OR of MSG_* constants such as Socket::MSG_TRUNC.
-
# It will be nil if the system uses 4.3BSD style old recvmsg system call.
-
#
-
# _controls_ is ancillary data which is an array of Socket::AncillaryData objects such as:
-
#
-
# #<Socket::AncillaryData: AF_UNIX SOCKET RIGHTS 7>
-
#
-
# _maxmesglen_ and _maxcontrollen_ can be nil.
-
# In that case, the buffer will be grown until the message is not truncated.
-
# Internally, MSG_PEEK is used.
-
# Buffer full and MSG_CTRUNC are checked for truncation.
-
#
-
# recvmsg can be used to implement recv_io as follows:
-
#
-
# mesg, sender_sockaddr, rflags, *controls = sock.recvmsg(:scm_rights=>true)
-
# controls.each {|ancdata|
-
# if ancdata.cmsg_is?(:SOCKET, :RIGHTS)
-
# return ancdata.unix_rights[0]
-
# end
-
# }
-
1
def recvmsg(dlen = nil, flags = 0, clen = nil, scm_rights: false)
-
__recvmsg(dlen, flags, clen, scm_rights)
-
end
-
-
# call-seq:
-
# basicsocket.recvmsg_nonblock(maxdatalen=nil, flags=0, maxcontrollen=nil, opts={}) => [data, sender_addrinfo, rflags, *controls]
-
#
-
# recvmsg receives a message using recvmsg(2) system call in non-blocking manner.
-
#
-
# It is similar to BasicSocket#recvmsg
-
# but non-blocking flag is set before the system call
-
# and it doesn't retry the system call.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that recvmsg_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
1
def recvmsg_nonblock(dlen = nil, flags = 0, clen = nil,
-
scm_rights: false, exception: true)
-
__recvmsg_nonblock(dlen, flags, clen, scm_rights, exception)
-
end
-
-
# Linux-specific optimizations to avoid fcntl for IO#read_nonblock
-
# and IO#write_nonblock using MSG_DONTWAIT
-
# Do other platforms support MSG_DONTWAIT reliably?
-
1
if RUBY_PLATFORM =~ /linux/ && Socket.const_defined?(:MSG_DONTWAIT)
-
1
def read_nonblock(len, str = nil, exception: true) # :nodoc:
-
__read_nonblock(len, str, exception)
-
end
-
-
1
def write_nonblock(buf, exception: true) # :nodoc:
-
__write_nonblock(buf, exception)
-
end
-
end
-
end
-
-
1
class Socket < BasicSocket
-
# enable the socket option IPV6_V6ONLY if IPV6_V6ONLY is available.
-
1
def ipv6only!
-
if defined? Socket::IPV6_V6ONLY
-
self.setsockopt(:IPV6, :V6ONLY, 1)
-
end
-
end
-
-
# call-seq:
-
# socket.recvfrom_nonblock(maxlen[, flags[, outbuf[, opts]]]) => [mesg, sender_addrinfo]
-
#
-
# Receives up to _maxlen_ bytes from +socket+ using recvfrom(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# _flags_ is zero or more of the +MSG_+ options.
-
# The first element of the results, _mesg_, is the data received.
-
# The second element, _sender_addrinfo_, contains protocol-specific address
-
# information of the sender.
-
#
-
# When recvfrom(2) returns 0, Socket#recvfrom_nonblock returns
-
# an empty string as data.
-
# The meaning depends on the socket: EOF on TCP, empty packet on UDP, etc.
-
#
-
# === Parameters
-
# * +maxlen+ - the maximum number of bytes to receive from the socket
-
# * +flags+ - zero or more of the +MSG_+ options
-
# * +outbuf+ - destination String buffer
-
# * +opts+ - keyword hash, supporting `exception: false`
-
#
-
# === Example
-
# # In one file, start this first
-
# require 'socket'
-
# include Socket::Constants
-
# socket = Socket.new(AF_INET, SOCK_STREAM, 0)
-
# sockaddr = Socket.sockaddr_in(2200, 'localhost')
-
# socket.bind(sockaddr)
-
# socket.listen(5)
-
# client, client_addrinfo = socket.accept
-
# begin # emulate blocking recvfrom
-
# pair = client.recvfrom_nonblock(20)
-
# rescue IO::WaitReadable
-
# IO.select([client])
-
# retry
-
# end
-
# data = pair[0].chomp
-
# puts "I only received 20 bytes '#{data}'"
-
# sleep 1
-
# socket.close
-
#
-
# # In another file, start this second
-
# require 'socket'
-
# include Socket::Constants
-
# socket = Socket.new(AF_INET, SOCK_STREAM, 0)
-
# sockaddr = Socket.sockaddr_in(2200, 'localhost')
-
# socket.connect(sockaddr)
-
# socket.puts "Watch this get cut short!"
-
# socket.close
-
#
-
# Refer to Socket#recvfrom for the exceptions that may be thrown if the call
-
# to _recvfrom_nonblock_ fails.
-
#
-
# Socket#recvfrom_nonblock may raise any error corresponding to recvfrom(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK or Errno::EAGAIN,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying
-
# recvfrom_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that recvfrom_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * Socket#recvfrom
-
1
def recvfrom_nonblock(len, flag = 0, str = nil, exception: true)
-
__recvfrom_nonblock(len, flag, str, exception)
-
end
-
-
# call-seq:
-
# socket.accept_nonblock([options]) => [client_socket, client_addrinfo]
-
#
-
# Accepts an incoming connection using accept(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# It returns an array containing the accepted socket
-
# for the incoming connection, _client_socket_,
-
# and an Addrinfo, _client_addrinfo_.
-
#
-
# === Example
-
# # In one script, start this first
-
# require 'socket'
-
# include Socket::Constants
-
# socket = Socket.new(AF_INET, SOCK_STREAM, 0)
-
# sockaddr = Socket.sockaddr_in(2200, 'localhost')
-
# socket.bind(sockaddr)
-
# socket.listen(5)
-
# begin # emulate blocking accept
-
# client_socket, client_addrinfo = socket.accept_nonblock
-
# rescue IO::WaitReadable, Errno::EINTR
-
# IO.select([socket])
-
# retry
-
# end
-
# puts "The client said, '#{client_socket.readline.chomp}'"
-
# client_socket.puts "Hello from script one!"
-
# socket.close
-
#
-
# # In another script, start this second
-
# require 'socket'
-
# include Socket::Constants
-
# socket = Socket.new(AF_INET, SOCK_STREAM, 0)
-
# sockaddr = Socket.sockaddr_in(2200, 'localhost')
-
# socket.connect(sockaddr)
-
# socket.puts "Hello from script 2."
-
# puts "The server said, '#{socket.readline.chomp}'"
-
# socket.close
-
#
-
# Refer to Socket#accept for the exceptions that may be thrown if the call
-
# to _accept_nonblock_ fails.
-
#
-
# Socket#accept_nonblock may raise any error corresponding to accept(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::ECONNABORTED or Errno::EPROTO,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying accept_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that accept_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * Socket#accept
-
1
def accept_nonblock(exception: true)
-
__accept_nonblock(exception)
-
end
-
-
# :call-seq:
-
# Socket.tcp(host, port, local_host=nil, local_port=nil, [opts]) {|socket| ... }
-
# Socket.tcp(host, port, local_host=nil, local_port=nil, [opts])
-
#
-
# creates a new socket object connected to host:port using TCP/IP.
-
#
-
# If local_host:local_port is given,
-
# the socket is bound to it.
-
#
-
# The optional last argument _opts_ is options represented by a hash.
-
# _opts_ may have following options:
-
#
-
# [:connect_timeout] specify the timeout in seconds.
-
# [:resolv_timeout] specify the name resolution timeout in seconds.
-
#
-
# If a block is given, the block is called with the socket.
-
# The value of the block is returned.
-
# The socket is closed when this method returns.
-
#
-
# If no block is given, the socket is returned.
-
#
-
# Socket.tcp("www.ruby-lang.org", 80) {|sock|
-
# sock.print "GET / HTTP/1.0\r\nHost: www.ruby-lang.org\r\n\r\n"
-
# sock.close_write
-
# puts sock.read
-
# }
-
#
-
1
def self.tcp(host, port, local_host = nil, local_port = nil, connect_timeout: nil, resolv_timeout: nil) # :yield: socket
-
last_error = nil
-
ret = nil
-
-
local_addr_list = nil
-
if local_host != nil || local_port != nil
-
local_addr_list = Addrinfo.getaddrinfo(local_host, local_port, nil, :STREAM, nil)
-
end
-
-
Addrinfo.foreach(host, port, nil, :STREAM, timeout: resolv_timeout) {|ai|
-
if local_addr_list
-
local_addr = local_addr_list.find {|local_ai| local_ai.afamily == ai.afamily }
-
next unless local_addr
-
else
-
local_addr = nil
-
end
-
begin
-
sock = local_addr ?
-
ai.connect_from(local_addr, timeout: connect_timeout) :
-
ai.connect(timeout: connect_timeout)
-
rescue SystemCallError
-
last_error = $!
-
next
-
end
-
ret = sock
-
break
-
}
-
unless ret
-
if last_error
-
raise last_error
-
else
-
raise SocketError, "no appropriate local address"
-
end
-
end
-
if block_given?
-
begin
-
yield ret
-
ensure
-
ret.close
-
end
-
else
-
ret
-
end
-
end
-
-
# :stopdoc:
-
1
def self.ip_sockets_port0(ai_list, reuseaddr)
-
sockets = []
-
begin
-
sockets.clear
-
port = nil
-
ai_list.each {|ai|
-
begin
-
s = Socket.new(ai.pfamily, ai.socktype, ai.protocol)
-
rescue SystemCallError
-
next
-
end
-
sockets << s
-
s.ipv6only! if ai.ipv6?
-
if reuseaddr
-
s.setsockopt(:SOCKET, :REUSEADDR, 1)
-
end
-
unless port
-
s.bind(ai)
-
port = s.local_address.ip_port
-
else
-
s.bind(ai.family_addrinfo(ai.ip_address, port))
-
end
-
}
-
rescue Errno::EADDRINUSE
-
sockets.each(&:close)
-
retry
-
rescue Exception
-
sockets.each(&:close)
-
raise
-
end
-
sockets
-
end
-
1
class << self
-
1
private :ip_sockets_port0
-
end
-
-
1
def self.tcp_server_sockets_port0(host)
-
ai_list = Addrinfo.getaddrinfo(host, 0, nil, :STREAM, nil, Socket::AI_PASSIVE)
-
sockets = ip_sockets_port0(ai_list, true)
-
begin
-
sockets.each {|s|
-
s.listen(Socket::SOMAXCONN)
-
}
-
rescue Exception
-
sockets.each(&:close)
-
raise
-
end
-
sockets
-
end
-
1
class << self
-
1
private :tcp_server_sockets_port0
-
end
-
# :startdoc:
-
-
# creates TCP/IP server sockets for _host_ and _port_.
-
# _host_ is optional.
-
#
-
# If no block given,
-
# it returns an array of listening sockets.
-
#
-
# If a block is given, the block is called with the sockets.
-
# The value of the block is returned.
-
# The socket is closed when this method returns.
-
#
-
# If _port_ is 0, actual port number is chosen dynamically.
-
# However all sockets in the result has same port number.
-
#
-
# # tcp_server_sockets returns two sockets.
-
# sockets = Socket.tcp_server_sockets(1296)
-
# p sockets #=> [#<Socket:fd 3>, #<Socket:fd 4>]
-
#
-
# # The sockets contains IPv6 and IPv4 sockets.
-
# sockets.each {|s| p s.local_address }
-
# #=> #<Addrinfo: [::]:1296 TCP>
-
# # #<Addrinfo: 0.0.0.0:1296 TCP>
-
#
-
# # IPv6 and IPv4 socket has same port number, 53114, even if it is chosen dynamically.
-
# sockets = Socket.tcp_server_sockets(0)
-
# sockets.each {|s| p s.local_address }
-
# #=> #<Addrinfo: [::]:53114 TCP>
-
# # #<Addrinfo: 0.0.0.0:53114 TCP>
-
#
-
# # The block is called with the sockets.
-
# Socket.tcp_server_sockets(0) {|sockets|
-
# p sockets #=> [#<Socket:fd 3>, #<Socket:fd 4>]
-
# }
-
#
-
1
def self.tcp_server_sockets(host=nil, port)
-
if port == 0
-
sockets = tcp_server_sockets_port0(host)
-
else
-
last_error = nil
-
sockets = []
-
begin
-
Addrinfo.foreach(host, port, nil, :STREAM, nil, Socket::AI_PASSIVE) {|ai|
-
begin
-
s = ai.listen
-
rescue SystemCallError
-
last_error = $!
-
next
-
end
-
sockets << s
-
}
-
if sockets.empty?
-
raise last_error
-
end
-
rescue Exception
-
sockets.each(&:close)
-
raise
-
end
-
end
-
if block_given?
-
begin
-
yield sockets
-
ensure
-
sockets.each(&:close)
-
end
-
else
-
sockets
-
end
-
end
-
-
# yield socket and client address for each a connection accepted via given sockets.
-
#
-
# The arguments are a list of sockets.
-
# The individual argument should be a socket or an array of sockets.
-
#
-
# This method yields the block sequentially.
-
# It means that the next connection is not accepted until the block returns.
-
# So concurrent mechanism, thread for example, should be used to service multiple clients at a time.
-
#
-
1
def self.accept_loop(*sockets) # :yield: socket, client_addrinfo
-
sockets.flatten!(1)
-
if sockets.empty?
-
raise ArgumentError, "no sockets"
-
end
-
loop {
-
readable, _, _ = IO.select(sockets)
-
readable.each {|r|
-
sock, addr = r.accept_nonblock(exception: false)
-
next if sock == :wait_readable
-
yield sock, addr
-
}
-
}
-
end
-
-
# creates a TCP/IP server on _port_ and calls the block for each connection accepted.
-
# The block is called with a socket and a client_address as an Addrinfo object.
-
#
-
# If _host_ is specified, it is used with _port_ to determine the server addresses.
-
#
-
# The socket is *not* closed when the block returns.
-
# So application should close it explicitly.
-
#
-
# This method calls the block sequentially.
-
# It means that the next connection is not accepted until the block returns.
-
# So concurrent mechanism, thread for example, should be used to service multiple clients at a time.
-
#
-
# Note that Addrinfo.getaddrinfo is used to determine the server socket addresses.
-
# When Addrinfo.getaddrinfo returns two or more addresses,
-
# IPv4 and IPv6 address for example,
-
# all of them are used.
-
# Socket.tcp_server_loop succeeds if one socket can be used at least.
-
#
-
# # Sequential echo server.
-
# # It services only one client at a time.
-
# Socket.tcp_server_loop(16807) {|sock, client_addrinfo|
-
# begin
-
# IO.copy_stream(sock, sock)
-
# ensure
-
# sock.close
-
# end
-
# }
-
#
-
# # Threaded echo server
-
# # It services multiple clients at a time.
-
# # Note that it may accept connections too much.
-
# Socket.tcp_server_loop(16807) {|sock, client_addrinfo|
-
# Thread.new {
-
# begin
-
# IO.copy_stream(sock, sock)
-
# ensure
-
# sock.close
-
# end
-
# }
-
# }
-
#
-
1
def self.tcp_server_loop(host=nil, port, &b) # :yield: socket, client_addrinfo
-
tcp_server_sockets(host, port) {|sockets|
-
accept_loop(sockets, &b)
-
}
-
end
-
-
# :call-seq:
-
# Socket.udp_server_sockets([host, ] port)
-
#
-
# Creates UDP/IP sockets for a UDP server.
-
#
-
# If no block given, it returns an array of sockets.
-
#
-
# If a block is given, the block is called with the sockets.
-
# The value of the block is returned.
-
# The sockets are closed when this method returns.
-
#
-
# If _port_ is zero, some port is chosen.
-
# But the chosen port is used for the all sockets.
-
#
-
# # UDP/IP echo server
-
# Socket.udp_server_sockets(0) {|sockets|
-
# p sockets.first.local_address.ip_port #=> 32963
-
# Socket.udp_server_loop_on(sockets) {|msg, msg_src|
-
# msg_src.reply msg
-
# }
-
# }
-
#
-
1
def self.udp_server_sockets(host=nil, port)
-
last_error = nil
-
sockets = []
-
-
ipv6_recvpktinfo = nil
-
if defined? Socket::AncillaryData
-
if defined? Socket::IPV6_RECVPKTINFO # RFC 3542
-
ipv6_recvpktinfo = Socket::IPV6_RECVPKTINFO
-
elsif defined? Socket::IPV6_PKTINFO # RFC 2292
-
ipv6_recvpktinfo = Socket::IPV6_PKTINFO
-
end
-
end
-
-
local_addrs = Socket.ip_address_list
-
-
ip_list = []
-
Addrinfo.foreach(host, port, nil, :DGRAM, nil, Socket::AI_PASSIVE) {|ai|
-
if ai.ipv4? && ai.ip_address == "0.0.0.0"
-
local_addrs.each {|a|
-
next unless a.ipv4?
-
ip_list << Addrinfo.new(a.to_sockaddr, :INET, :DGRAM, 0);
-
}
-
elsif ai.ipv6? && ai.ip_address == "::" && !ipv6_recvpktinfo
-
local_addrs.each {|a|
-
next unless a.ipv6?
-
ip_list << Addrinfo.new(a.to_sockaddr, :INET6, :DGRAM, 0);
-
}
-
else
-
ip_list << ai
-
end
-
}
-
ip_list.uniq!(&:to_sockaddr)
-
-
if port == 0
-
sockets = ip_sockets_port0(ip_list, false)
-
else
-
ip_list.each {|ip|
-
ai = Addrinfo.udp(ip.ip_address, port)
-
begin
-
s = ai.bind
-
rescue SystemCallError
-
last_error = $!
-
next
-
end
-
sockets << s
-
}
-
if sockets.empty?
-
raise last_error
-
end
-
end
-
-
sockets.each {|s|
-
ai = s.local_address
-
if ipv6_recvpktinfo && ai.ipv6? && ai.ip_address == "::"
-
s.setsockopt(:IPV6, ipv6_recvpktinfo, 1)
-
end
-
}
-
-
if block_given?
-
begin
-
yield sockets
-
ensure
-
sockets.each(&:close) if sockets
-
end
-
else
-
sockets
-
end
-
end
-
-
# :call-seq:
-
# Socket.udp_server_recv(sockets) {|msg, msg_src| ... }
-
#
-
# Receive UDP/IP packets from the given _sockets_.
-
# For each packet received, the block is called.
-
#
-
# The block receives _msg_ and _msg_src_.
-
# _msg_ is a string which is the payload of the received packet.
-
# _msg_src_ is a Socket::UDPSource object which is used for reply.
-
#
-
# Socket.udp_server_loop can be implemented using this method as follows.
-
#
-
# udp_server_sockets(host, port) {|sockets|
-
# loop {
-
# readable, _, _ = IO.select(sockets)
-
# udp_server_recv(readable) {|msg, msg_src| ... }
-
# }
-
# }
-
#
-
1
def self.udp_server_recv(sockets)
-
sockets.each {|r|
-
msg, sender_addrinfo, _, *controls = r.recvmsg_nonblock(exception: false)
-
next if msg == :wait_readable
-
ai = r.local_address
-
if ai.ipv6? and pktinfo = controls.find {|c| c.cmsg_is?(:IPV6, :PKTINFO) }
-
ai = Addrinfo.udp(pktinfo.ipv6_pktinfo_addr.ip_address, ai.ip_port)
-
yield msg, UDPSource.new(sender_addrinfo, ai) {|reply_msg|
-
r.sendmsg reply_msg, 0, sender_addrinfo, pktinfo
-
}
-
else
-
yield msg, UDPSource.new(sender_addrinfo, ai) {|reply_msg|
-
r.send reply_msg, 0, sender_addrinfo
-
}
-
end
-
}
-
end
-
-
# :call-seq:
-
# Socket.udp_server_loop_on(sockets) {|msg, msg_src| ... }
-
#
-
# Run UDP/IP server loop on the given sockets.
-
#
-
# The return value of Socket.udp_server_sockets is appropriate for the argument.
-
#
-
# It calls the block for each message received.
-
#
-
1
def self.udp_server_loop_on(sockets, &b) # :yield: msg, msg_src
-
loop {
-
readable, _, _ = IO.select(sockets)
-
udp_server_recv(readable, &b)
-
}
-
end
-
-
# :call-seq:
-
# Socket.udp_server_loop(port) {|msg, msg_src| ... }
-
# Socket.udp_server_loop(host, port) {|msg, msg_src| ... }
-
#
-
# creates a UDP/IP server on _port_ and calls the block for each message arrived.
-
# The block is called with the message and its source information.
-
#
-
# This method allocates sockets internally using _port_.
-
# If _host_ is specified, it is used conjunction with _port_ to determine the server addresses.
-
#
-
# The _msg_ is a string.
-
#
-
# The _msg_src_ is a Socket::UDPSource object.
-
# It is used for reply.
-
#
-
# # UDP/IP echo server.
-
# Socket.udp_server_loop(9261) {|msg, msg_src|
-
# msg_src.reply msg
-
# }
-
#
-
1
def self.udp_server_loop(host=nil, port, &b) # :yield: message, message_source
-
udp_server_sockets(host, port) {|sockets|
-
udp_server_loop_on(sockets, &b)
-
}
-
end
-
-
# UDP/IP address information used by Socket.udp_server_loop.
-
1
class UDPSource
-
# +remote_address+ is an Addrinfo object.
-
#
-
# +local_address+ is an Addrinfo object.
-
#
-
# +reply_proc+ is a Proc used to send reply back to the source.
-
1
def initialize(remote_address, local_address, &reply_proc)
-
@remote_address = remote_address
-
@local_address = local_address
-
@reply_proc = reply_proc
-
end
-
-
# Address of the source
-
1
attr_reader :remote_address
-
-
# Local address
-
1
attr_reader :local_address
-
-
1
def inspect # :nodoc:
-
"\#<#{self.class}: #{@remote_address.inspect_sockaddr} to #{@local_address.inspect_sockaddr}>".dup
-
end
-
-
# Sends the String +msg+ to the source
-
1
def reply(msg)
-
@reply_proc.call msg
-
end
-
end
-
-
# creates a new socket connected to path using UNIX socket socket.
-
#
-
# If a block is given, the block is called with the socket.
-
# The value of the block is returned.
-
# The socket is closed when this method returns.
-
#
-
# If no block is given, the socket is returned.
-
#
-
# # talk to /tmp/sock socket.
-
# Socket.unix("/tmp/sock") {|sock|
-
# t = Thread.new { IO.copy_stream(sock, STDOUT) }
-
# IO.copy_stream(STDIN, sock)
-
# t.join
-
# }
-
#
-
1
def self.unix(path) # :yield: socket
-
addr = Addrinfo.unix(path)
-
sock = addr.connect
-
if block_given?
-
begin
-
yield sock
-
ensure
-
sock.close
-
end
-
else
-
sock
-
end
-
end
-
-
# creates a UNIX server socket on _path_
-
#
-
# If no block given, it returns a listening socket.
-
#
-
# If a block is given, it is called with the socket and the block value is returned.
-
# When the block exits, the socket is closed and the socket file is removed.
-
#
-
# socket = Socket.unix_server_socket("/tmp/s")
-
# p socket #=> #<Socket:fd 3>
-
# p socket.local_address #=> #<Addrinfo: /tmp/s SOCK_STREAM>
-
#
-
# Socket.unix_server_socket("/tmp/sock") {|s|
-
# p s #=> #<Socket:fd 3>
-
# p s.local_address #=> # #<Addrinfo: /tmp/sock SOCK_STREAM>
-
# }
-
#
-
1
def self.unix_server_socket(path)
-
unless unix_socket_abstract_name?(path)
-
begin
-
st = File.lstat(path)
-
rescue Errno::ENOENT
-
end
-
if st&.socket? && st.owned?
-
File.unlink path
-
end
-
end
-
s = Addrinfo.unix(path).listen
-
if block_given?
-
begin
-
yield s
-
ensure
-
s.close
-
unless unix_socket_abstract_name?(path)
-
File.unlink path
-
end
-
end
-
else
-
s
-
end
-
end
-
-
1
class << self
-
1
private
-
-
1
def unix_socket_abstract_name?(path)
-
/linux/ =~ RUBY_PLATFORM && /\A(\0|\z)/ =~ path
-
end
-
end
-
-
# creates a UNIX socket server on _path_.
-
# It calls the block for each socket accepted.
-
#
-
# If _host_ is specified, it is used with _port_ to determine the server ports.
-
#
-
# The socket is *not* closed when the block returns.
-
# So application should close it.
-
#
-
# This method deletes the socket file pointed by _path_ at first if
-
# the file is a socket file and it is owned by the user of the application.
-
# This is safe only if the directory of _path_ is not changed by a malicious user.
-
# So don't use /tmp/malicious-users-directory/socket.
-
# Note that /tmp/socket and /tmp/your-private-directory/socket is safe assuming that /tmp has sticky bit.
-
#
-
# # Sequential echo server.
-
# # It services only one client at a time.
-
# Socket.unix_server_loop("/tmp/sock") {|sock, client_addrinfo|
-
# begin
-
# IO.copy_stream(sock, sock)
-
# ensure
-
# sock.close
-
# end
-
# }
-
#
-
1
def self.unix_server_loop(path, &b) # :yield: socket, client_addrinfo
-
unix_server_socket(path) {|serv|
-
accept_loop(serv, &b)
-
}
-
end
-
-
# call-seq:
-
# socket.connect_nonblock(remote_sockaddr, [options]) => 0
-
#
-
# Requests a connection to be made on the given +remote_sockaddr+ after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# Returns 0 if successful, otherwise an exception is raised.
-
#
-
# === Parameter
-
# # +remote_sockaddr+ - the +struct+ sockaddr contained in a string or Addrinfo object
-
#
-
# === Example:
-
# # Pull down Google's web page
-
# require 'socket'
-
# include Socket::Constants
-
# socket = Socket.new(AF_INET, SOCK_STREAM, 0)
-
# sockaddr = Socket.sockaddr_in(80, 'www.google.com')
-
# begin # emulate blocking connect
-
# socket.connect_nonblock(sockaddr)
-
# rescue IO::WaitWritable
-
# IO.select(nil, [socket]) # wait 3-way handshake completion
-
# begin
-
# socket.connect_nonblock(sockaddr) # check connection failure
-
# rescue Errno::EISCONN
-
# end
-
# end
-
# socket.write("GET / HTTP/1.0\r\n\r\n")
-
# results = socket.read
-
#
-
# Refer to Socket#connect for the exceptions that may be thrown if the call
-
# to _connect_nonblock_ fails.
-
#
-
# Socket#connect_nonblock may raise any error corresponding to connect(2) failure,
-
# including Errno::EINPROGRESS.
-
#
-
# If the exception is Errno::EINPROGRESS,
-
# it is extended by IO::WaitWritable.
-
# So IO::WaitWritable can be used to rescue the exceptions for retrying connect_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that connect_nonblock should not raise an IO::WaitWritable exception, but
-
# return the symbol +:wait_writable+ instead.
-
#
-
# === See
-
# # Socket#connect
-
1
def connect_nonblock(addr, exception: true)
-
__connect_nonblock(addr, exception)
-
end
-
end
-
-
1
class UDPSocket < IPSocket
-
-
# call-seq:
-
# udpsocket.recvfrom_nonblock(maxlen [, flags[, outbuf [, options]]]) => [mesg, sender_inet_addr]
-
#
-
# Receives up to _maxlen_ bytes from +udpsocket+ using recvfrom(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# _flags_ is zero or more of the +MSG_+ options.
-
# The first element of the results, _mesg_, is the data received.
-
# The second element, _sender_inet_addr_, is an array to represent the sender address.
-
#
-
# When recvfrom(2) returns 0,
-
# Socket#recvfrom_nonblock returns an empty string as data.
-
# It means an empty packet.
-
#
-
# === Parameters
-
# * +maxlen+ - the number of bytes to receive from the socket
-
# * +flags+ - zero or more of the +MSG_+ options
-
# * +outbuf+ - destination String buffer
-
# * +options+ - keyword hash, supporting `exception: false`
-
#
-
# === Example
-
# require 'socket'
-
# s1 = UDPSocket.new
-
# s1.bind("127.0.0.1", 0)
-
# s2 = UDPSocket.new
-
# s2.bind("127.0.0.1", 0)
-
# s2.connect(*s1.addr.values_at(3,1))
-
# s1.connect(*s2.addr.values_at(3,1))
-
# s1.send "aaa", 0
-
# begin # emulate blocking recvfrom
-
# p s2.recvfrom_nonblock(10) #=> ["aaa", ["AF_INET", 33302, "localhost.localdomain", "127.0.0.1"]]
-
# rescue IO::WaitReadable
-
# IO.select([s2])
-
# retry
-
# end
-
#
-
# Refer to Socket#recvfrom for the exceptions that may be thrown if the call
-
# to _recvfrom_nonblock_ fails.
-
#
-
# UDPSocket#recvfrom_nonblock may raise any error corresponding to recvfrom(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK or Errno::EAGAIN,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying recvfrom_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that recvfrom_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * Socket#recvfrom
-
1
def recvfrom_nonblock(len, flag = 0, outbuf = nil, exception: true)
-
__recvfrom_nonblock(len, flag, outbuf, exception)
-
end
-
end
-
-
1
class TCPServer < TCPSocket
-
-
# call-seq:
-
# tcpserver.accept_nonblock([options]) => tcpsocket
-
#
-
# Accepts an incoming connection using accept(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# It returns an accepted TCPSocket for the incoming connection.
-
#
-
# === Example
-
# require 'socket'
-
# serv = TCPServer.new(2202)
-
# begin # emulate blocking accept
-
# sock = serv.accept_nonblock
-
# rescue IO::WaitReadable, Errno::EINTR
-
# IO.select([serv])
-
# retry
-
# end
-
# # sock is an accepted socket.
-
#
-
# Refer to Socket#accept for the exceptions that may be thrown if the call
-
# to TCPServer#accept_nonblock fails.
-
#
-
# TCPServer#accept_nonblock may raise any error corresponding to accept(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::ECONNABORTED, Errno::EPROTO,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying accept_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that accept_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * TCPServer#accept
-
# * Socket#accept
-
1
def accept_nonblock(exception: true)
-
__accept_nonblock(exception)
-
end
-
end
-
-
class UNIXServer < UNIXSocket
-
# call-seq:
-
# unixserver.accept_nonblock([options]) => unixsocket
-
#
-
# Accepts an incoming connection using accept(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# It returns an accepted UNIXSocket for the incoming connection.
-
#
-
# === Example
-
# require 'socket'
-
# serv = UNIXServer.new("/tmp/sock")
-
# begin # emulate blocking accept
-
# sock = serv.accept_nonblock
-
# rescue IO::WaitReadable, Errno::EINTR
-
# IO.select([serv])
-
# retry
-
# end
-
# # sock is an accepted socket.
-
#
-
# Refer to Socket#accept for the exceptions that may be thrown if the call
-
# to UNIXServer#accept_nonblock fails.
-
#
-
# UNIXServer#accept_nonblock may raise any error corresponding to accept(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::ECONNABORTED or Errno::EPROTO,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying accept_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that accept_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * UNIXServer#accept
-
# * Socket#accept
-
1
def accept_nonblock(exception: true)
-
__accept_nonblock(exception)
-
end
-
1
end if defined?(UNIXSocket)
-
# frozen_string_literal: true
-
#
-
# tempfile - manipulates temporary files
-
#
-
# $Id$
-
#
-
-
1
require 'delegate'
-
1
require 'tmpdir'
-
-
# A utility class for managing temporary files. When you create a Tempfile
-
# object, it will create a temporary file with a unique filename. A Tempfile
-
# objects behaves just like a File object, and you can perform all the usual
-
# file operations on it: reading data, writing data, changing its permissions,
-
# etc. So although this class does not explicitly document all instance methods
-
# supported by File, you can in fact call any File instance method on a
-
# Tempfile object.
-
#
-
# == Synopsis
-
#
-
# require 'tempfile'
-
#
-
# file = Tempfile.new('foo')
-
# file.path # => A unique filename in the OS's temp directory,
-
# # e.g.: "/tmp/foo.24722.0"
-
# # This filename contains 'foo' in its basename.
-
# file.write("hello world")
-
# file.rewind
-
# file.read # => "hello world"
-
# file.close
-
# file.unlink # deletes the temp file
-
#
-
# == Good practices
-
#
-
# === Explicit close
-
#
-
# When a Tempfile object is garbage collected, or when the Ruby interpreter
-
# exits, its associated temporary file is automatically deleted. This means
-
# that's it's unnecessary to explicitly delete a Tempfile after use, though
-
# it's good practice to do so: not explicitly deleting unused Tempfiles can
-
# potentially leave behind large amounts of tempfiles on the filesystem
-
# until they're garbage collected. The existence of these temp files can make
-
# it harder to determine a new Tempfile filename.
-
#
-
# Therefore, one should always call #unlink or close in an ensure block, like
-
# this:
-
#
-
# file = Tempfile.new('foo')
-
# begin
-
# # ...do something with file...
-
# ensure
-
# file.close
-
# file.unlink # deletes the temp file
-
# end
-
#
-
# === Unlink after creation
-
#
-
# On POSIX systems, it's possible to unlink a file right after creating it,
-
# and before closing it. This removes the filesystem entry without closing
-
# the file handle, so it ensures that only the processes that already had
-
# the file handle open can access the file's contents. It's strongly
-
# recommended that you do this if you do not want any other processes to
-
# be able to read from or write to the Tempfile, and you do not need to
-
# know the Tempfile's filename either.
-
#
-
# For example, a practical use case for unlink-after-creation would be this:
-
# you need a large byte buffer that's too large to comfortably fit in RAM,
-
# e.g. when you're writing a web server and you want to buffer the client's
-
# file upload data.
-
#
-
# Please refer to #unlink for more information and a code example.
-
#
-
# == Minor notes
-
#
-
# Tempfile's filename picking method is both thread-safe and inter-process-safe:
-
# it guarantees that no other threads or processes will pick the same filename.
-
#
-
# Tempfile itself however may not be entirely thread-safe. If you access the
-
# same Tempfile object from multiple threads then you should protect it with a
-
# mutex.
-
1
class Tempfile < DelegateClass(File)
-
# Creates a temporary file with permissions 0600 (= only readable and
-
# writable by the owner) and opens it with mode "w+".
-
#
-
# The +basename+ parameter is used to determine the name of the
-
# temporary file. You can either pass a String or an Array with
-
# 2 String elements. In the former form, the temporary file's base
-
# name will begin with the given string. In the latter form,
-
# the temporary file's base name will begin with the array's first
-
# element, and end with the second element. For example:
-
#
-
# file = Tempfile.new('hello')
-
# file.path # => something like: "/tmp/hello2843-8392-92849382--0"
-
#
-
# # Use the Array form to enforce an extension in the filename:
-
# file = Tempfile.new(['hello', '.jpg'])
-
# file.path # => something like: "/tmp/hello2843-8392-92849382--0.jpg"
-
#
-
# The temporary file will be placed in the directory as specified
-
# by the +tmpdir+ parameter. By default, this is +Dir.tmpdir+.
-
#
-
# file = Tempfile.new('hello', '/home/aisaka')
-
# file.path # => something like: "/home/aisaka/hello2843-8392-92849382--0"
-
#
-
# You can also pass an options hash. Under the hood, Tempfile creates
-
# the temporary file using +File.open+. These options will be passed to
-
# +File.open+. This is mostly useful for specifying encoding
-
# options, e.g.:
-
#
-
# Tempfile.new('hello', '/home/aisaka', encoding: 'ascii-8bit')
-
#
-
# # You can also omit the 'tmpdir' parameter:
-
# Tempfile.new('hello', encoding: 'ascii-8bit')
-
#
-
# Note: +mode+ keyword argument, as accepted by Tempfile, can only be
-
# numeric, combination of the modes defined in File::Constants.
-
#
-
# === Exceptions
-
#
-
# If Tempfile.new cannot find a unique filename within a limited
-
# number of tries, then it will raise an exception.
-
1
def initialize(basename="", tmpdir=nil, mode: 0, **options)
-
warn "Tempfile.new doesn't call the given block.", uplevel: 1 if block_given?
-
-
@unlinked = false
-
@mode = mode|File::RDWR|File::CREAT|File::EXCL
-
::Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts|
-
opts[:perm] = 0600
-
@tmpfile = File.open(tmpname, @mode, **opts)
-
@opts = opts.freeze
-
end
-
ObjectSpace.define_finalizer(self, Remover.new(@tmpfile))
-
-
super(@tmpfile)
-
end
-
-
# Opens or reopens the file with mode "r+".
-
1
def open
-
_close
-
mode = @mode & ~(File::CREAT|File::EXCL)
-
@tmpfile = File.open(@tmpfile.path, mode, **@opts)
-
__setobj__(@tmpfile)
-
end
-
-
1
def _close # :nodoc:
-
@tmpfile.close
-
end
-
1
protected :_close
-
-
# Closes the file. If +unlink_now+ is true, then the file will be unlinked
-
# (deleted) after closing. Of course, you can choose to later call #unlink
-
# if you do not unlink it now.
-
#
-
# If you don't explicitly unlink the temporary file, the removal
-
# will be delayed until the object is finalized.
-
1
def close(unlink_now=false)
-
_close
-
unlink if unlink_now
-
end
-
-
# Closes and unlinks (deletes) the file. Has the same effect as called
-
# <tt>close(true)</tt>.
-
1
def close!
-
close(true)
-
end
-
-
# Unlinks (deletes) the file from the filesystem. One should always unlink
-
# the file after using it, as is explained in the "Explicit close" good
-
# practice section in the Tempfile overview:
-
#
-
# file = Tempfile.new('foo')
-
# begin
-
# # ...do something with file...
-
# ensure
-
# file.close
-
# file.unlink # deletes the temp file
-
# end
-
#
-
# === Unlink-before-close
-
#
-
# On POSIX systems it's possible to unlink a file before closing it. This
-
# practice is explained in detail in the Tempfile overview (section
-
# "Unlink after creation"); please refer there for more information.
-
#
-
# However, unlink-before-close may not be supported on non-POSIX operating
-
# systems. Microsoft Windows is the most notable case: unlinking a non-closed
-
# file will result in an error, which this method will silently ignore. If
-
# you want to practice unlink-before-close whenever possible, then you should
-
# write code like this:
-
#
-
# file = Tempfile.new('foo')
-
# file.unlink # On Windows this silently fails.
-
# begin
-
# # ... do something with file ...
-
# ensure
-
# file.close! # Closes the file handle. If the file wasn't unlinked
-
# # because #unlink failed, then this method will attempt
-
# # to do so again.
-
# end
-
1
def unlink
-
return if @unlinked
-
begin
-
File.unlink(@tmpfile.path)
-
rescue Errno::ENOENT
-
rescue Errno::EACCES
-
# may not be able to unlink on Windows; just ignore
-
return
-
end
-
ObjectSpace.undefine_finalizer(self)
-
@unlinked = true
-
end
-
1
alias delete unlink
-
-
# Returns the full path name of the temporary file.
-
# This will be nil if #unlink has been called.
-
1
def path
-
@unlinked ? nil : @tmpfile.path
-
end
-
-
# Returns the size of the temporary file. As a side effect, the IO
-
# buffer is flushed before determining the size.
-
1
def size
-
if !@tmpfile.closed?
-
@tmpfile.size # File#size calls rb_io_flush_raw()
-
else
-
File.size(@tmpfile.path)
-
end
-
end
-
1
alias length size
-
-
# :stopdoc:
-
1
def inspect
-
if @tmpfile.closed?
-
"#<#{self.class}:#{path} (closed)>"
-
else
-
"#<#{self.class}:#{path}>"
-
end
-
end
-
-
1
class Remover # :nodoc:
-
1
def initialize(tmpfile)
-
@pid = Process.pid
-
@tmpfile = tmpfile
-
end
-
-
1
def call(*args)
-
return if @pid != Process.pid
-
-
$stderr.puts "removing #{@tmpfile.path}..." if $DEBUG
-
-
@tmpfile.close
-
begin
-
File.unlink(@tmpfile.path)
-
rescue Errno::ENOENT
-
end
-
-
$stderr.puts "done" if $DEBUG
-
end
-
end
-
-
1
class << self
-
# :startdoc:
-
-
# Creates a new Tempfile.
-
#
-
# If no block is given, this is a synonym for Tempfile.new.
-
#
-
# If a block is given, then a Tempfile object will be constructed,
-
# and the block is run with said object as argument. The Tempfile
-
# object will be automatically closed after the block terminates.
-
# The call returns the value of the block.
-
#
-
# In any case, all arguments (<code>*args</code>) will be passed to Tempfile.new.
-
#
-
# Tempfile.open('foo', '/home/temp') do |f|
-
# # ... do something with f ...
-
# end
-
#
-
# # Equivalent:
-
# f = Tempfile.open('foo', '/home/temp')
-
# begin
-
# # ... do something with f ...
-
# ensure
-
# f.close
-
# end
-
1
def open(*args, **kw)
-
tempfile = new(*args, **kw)
-
-
if block_given?
-
begin
-
yield(tempfile)
-
ensure
-
tempfile.close
-
end
-
else
-
tempfile
-
end
-
end
-
end
-
end
-
-
# Creates a temporary file as usual File object (not Tempfile).
-
# It doesn't use finalizer and delegation.
-
#
-
# If no block is given, this is similar to Tempfile.new except
-
# creating File instead of Tempfile.
-
# The created file is not removed automatically.
-
# You should use File.unlink to remove it.
-
#
-
# If a block is given, then a File object will be constructed,
-
# and the block is invoked with the object as the argument.
-
# The File object will be automatically closed and
-
# the temporary file is removed after the block terminates.
-
# The call returns the value of the block.
-
#
-
# In any case, all arguments (+basename+, +tmpdir+, +mode+, and
-
# <code>**options</code>) will be treated as Tempfile.new.
-
#
-
# Tempfile.create('foo', '/home/temp') do |f|
-
# # ... do something with f ...
-
# end
-
#
-
1
def Tempfile.create(basename="", tmpdir=nil, mode: 0, **options)
-
tmpfile = nil
-
Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts|
-
mode |= File::RDWR|File::CREAT|File::EXCL
-
opts[:perm] = 0600
-
tmpfile = File.open(tmpname, mode, **opts)
-
end
-
if block_given?
-
begin
-
yield tmpfile
-
ensure
-
unless tmpfile.closed?
-
if File.identical?(tmpfile, tmpfile.path)
-
unlinked = File.unlink tmpfile.path rescue nil
-
end
-
tmpfile.close
-
end
-
unless unlinked
-
begin
-
File.unlink tmpfile.path
-
rescue Errno::ENOENT
-
end
-
end
-
end
-
else
-
tmpfile
-
end
-
end
-
# frozen_string_literal: false
-
# Timeout long-running blocks
-
#
-
# == Synopsis
-
#
-
# require 'timeout'
-
# status = Timeout::timeout(5) {
-
# # Something that should be interrupted if it takes more than 5 seconds...
-
# }
-
#
-
# == Description
-
#
-
# Timeout provides a way to auto-terminate a potentially long-running
-
# operation if it hasn't finished in a fixed amount of time.
-
#
-
# Previous versions didn't use a module for namespacing, however
-
# #timeout is provided for backwards compatibility. You
-
# should prefer Timeout.timeout instead.
-
#
-
# == Copyright
-
#
-
# Copyright:: (C) 2000 Network Applied Communication Laboratory, Inc.
-
# Copyright:: (C) 2000 Information-technology Promotion Agency, Japan
-
-
1
module Timeout
-
# Raised by Timeout.timeout when the block times out.
-
1
class Error < RuntimeError
-
1
attr_reader :thread
-
-
1
def self.catch(*args)
-
exc = new(*args)
-
exc.instance_variable_set(:@thread, Thread.current)
-
::Kernel.catch(exc) {yield exc}
-
end
-
-
1
def exception(*)
-
# TODO: use Fiber.current to see if self can be thrown
-
if self.thread == Thread.current
-
bt = caller
-
begin
-
throw(self, bt)
-
rescue UncaughtThrowError
-
end
-
end
-
self
-
end
-
end
-
-
# :stopdoc:
-
1
THIS_FILE = /\A#{Regexp.quote(__FILE__)}:/o
-
1
CALLER_OFFSET = ((c = caller[0]) && THIS_FILE =~ c) ? 1 : 0
-
1
private_constant :THIS_FILE, :CALLER_OFFSET
-
# :startdoc:
-
-
# Perform an operation in a block, raising an error if it takes longer than
-
# +sec+ seconds to complete.
-
#
-
# +sec+:: Number of seconds to wait for the block to terminate. Any number
-
# may be used, including Floats to specify fractional seconds. A
-
# value of 0 or +nil+ will execute the block without any timeout.
-
# +klass+:: Exception Class to raise if the block fails to terminate
-
# in +sec+ seconds. Omitting will use the default, Timeout::Error
-
# +message+:: Error message to raise with Exception Class.
-
# Omitting will use the default, "execution expired"
-
#
-
# Returns the result of the block *if* the block completed before
-
# +sec+ seconds, otherwise throws an exception, based on the value of +klass+.
-
#
-
# The exception thrown to terminate the given block cannot be rescued inside
-
# the block unless +klass+ is given explicitly. However, the block can use
-
# ensure to prevent the handling of the exception. For that reason, this
-
# method cannot be relied on to enforce timeouts for untrusted blocks.
-
#
-
# Note that this is both a method of module Timeout, so you can <tt>include
-
# Timeout</tt> into your classes so they have a #timeout method, as well as
-
# a module method, so you can call it directly as Timeout.timeout().
-
1
def timeout(sec, klass = nil, message = nil) #:yield: +sec+
-
return yield(sec) if sec == nil or sec.zero?
-
message ||= "execution expired".freeze
-
from = "from #{caller_locations(1, 1)[0]}" if $DEBUG
-
e = Error
-
bl = proc do |exception|
-
begin
-
x = Thread.current
-
y = Thread.start {
-
Thread.current.name = from
-
begin
-
sleep sec
-
rescue => e
-
x.raise e
-
else
-
x.raise exception, message
-
end
-
}
-
return yield(sec)
-
ensure
-
if y
-
y.kill
-
y.join # make sure y is dead.
-
end
-
end
-
end
-
if klass
-
begin
-
bl.call(klass)
-
rescue klass => e
-
bt = e.backtrace
-
end
-
else
-
bt = Error.catch(message, &bl)
-
end
-
level = -caller(CALLER_OFFSET).size-2
-
while THIS_FILE =~ bt[level]
-
bt.delete_at(level)
-
end
-
raise(e, message, bt)
-
end
-
-
1
module_function :timeout
-
end
-
-
1
def timeout(*args, &block)
-
warn "Object##{__method__} is deprecated, use Timeout.timeout instead.", uplevel: 1
-
Timeout.timeout(*args, &block)
-
end
-
-
# Another name for Timeout::Error, defined for backwards compatibility with
-
# earlier versions of timeout.rb.
-
1
TimeoutError = Timeout::Error
-
1
class Object
-
1
deprecate_constant :TimeoutError
-
end
-
# frozen_string_literal: true
-
#
-
# tmpdir - retrieve temporary directory path
-
#
-
# $Id$
-
#
-
-
1
require 'fileutils'
-
begin
-
1
require 'etc.so'
-
rescue LoadError # rescue LoadError for miniruby
-
end
-
-
1
class Dir
-
-
1
@@systmpdir ||= defined?(Etc.systmpdir) ? Etc.systmpdir : '/tmp'
-
-
##
-
# Returns the operating system's temporary file path.
-
-
1
def self.tmpdir
-
2
tmp = nil
-
2
[ENV['TMPDIR'], ENV['TMP'], ENV['TEMP'], @@systmpdir, '/tmp', '.'].each do |dir|
-
8
next if !dir
-
2
dir = File.expand_path(dir)
-
2
if stat = File.stat(dir) and stat.directory? and stat.writable? and
-
(!stat.world_writable? or stat.sticky?)
-
2
tmp = dir
-
2
break
-
end rescue nil
-
end
-
2
raise ArgumentError, "could not find a temporary directory" unless tmp
-
2
tmp
-
end
-
-
# Dir.mktmpdir creates a temporary directory.
-
#
-
# The directory is created with 0700 permission.
-
# Application should not change the permission to make the temporary directory accessible from other users.
-
#
-
# The prefix and suffix of the name of the directory is specified by
-
# the optional first argument, <i>prefix_suffix</i>.
-
# - If it is not specified or nil, "d" is used as the prefix and no suffix is used.
-
# - If it is a string, it is used as the prefix and no suffix is used.
-
# - If it is an array, first element is used as the prefix and second element is used as a suffix.
-
#
-
# Dir.mktmpdir {|dir| dir is ".../d..." }
-
# Dir.mktmpdir("foo") {|dir| dir is ".../foo..." }
-
# Dir.mktmpdir(["foo", "bar"]) {|dir| dir is ".../foo...bar" }
-
#
-
# The directory is created under Dir.tmpdir or
-
# the optional second argument <i>tmpdir</i> if non-nil value is given.
-
#
-
# Dir.mktmpdir {|dir| dir is "#{Dir.tmpdir}/d..." }
-
# Dir.mktmpdir(nil, "/var/tmp") {|dir| dir is "/var/tmp/d..." }
-
#
-
# If a block is given,
-
# it is yielded with the path of the directory.
-
# The directory and its contents are removed
-
# using FileUtils.remove_entry before Dir.mktmpdir returns.
-
# The value of the block is returned.
-
#
-
# Dir.mktmpdir {|dir|
-
# # use the directory...
-
# open("#{dir}/foo", "w") { ... }
-
# }
-
#
-
# If a block is not given,
-
# The path of the directory is returned.
-
# In this case, Dir.mktmpdir doesn't remove the directory.
-
#
-
# dir = Dir.mktmpdir
-
# begin
-
# # use the directory...
-
# open("#{dir}/foo", "w") { ... }
-
# ensure
-
# # remove the directory.
-
# FileUtils.remove_entry dir
-
# end
-
#
-
1
def self.mktmpdir(prefix_suffix=nil, *rest, **options)
-
base = nil
-
path = Tmpname.create(prefix_suffix || "d", *rest, **options) {|path, _, _, d|
-
base = d
-
mkdir(path, 0700)
-
}
-
if block_given?
-
begin
-
yield path
-
ensure
-
unless base
-
stat = File.stat(File.dirname(path))
-
if stat.world_writable? and !stat.sticky?
-
raise ArgumentError, "parent directory is world writable but not sticky"
-
end
-
end
-
FileUtils.remove_entry path
-
end
-
else
-
path
-
end
-
end
-
-
1
module Tmpname # :nodoc:
-
1
module_function
-
-
1
def tmpdir
-
2
Dir.tmpdir
-
end
-
-
1
UNUSABLE_CHARS = [File::SEPARATOR, File::ALT_SEPARATOR, File::PATH_SEPARATOR, ":"].uniq.join("").freeze
-
-
1
def create(basename, tmpdir=nil, max_try: nil, **opts)
-
2
origdir = tmpdir
-
2
tmpdir ||= tmpdir()
-
2
n = nil
-
2
prefix, suffix = basename
-
2
prefix = (String.try_convert(prefix) or
-
raise ArgumentError, "unexpected prefix: #{prefix.inspect}")
-
2
prefix = prefix.delete(UNUSABLE_CHARS)
-
2
suffix &&= (String.try_convert(suffix) or
-
raise ArgumentError, "unexpected suffix: #{suffix.inspect}")
-
2
suffix &&= suffix.delete(UNUSABLE_CHARS)
-
begin
-
2
t = Time.now.strftime("%Y%m%d")
-
2
path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"\
-
"#{n ? %[-#{n}] : ''}#{suffix||''}"
-
2
path = File.join(tmpdir, path)
-
2
yield(path, n, opts, origdir)
-
rescue Errno::EEXIST
-
n ||= 0
-
n += 1
-
retry if !max_try or n < max_try
-
raise "cannot generate temporary name using `#{basename}' under `#{tmpdir}'"
-
end
-
2
path
-
end
-
end
-
end
-
# frozen_string_literal: true
-
#--
-
# = uri/common.rb
-
#
-
# Author:: Akira Yamada <akira@ruby-lang.org>
-
# Revision:: $Id$
-
# License::
-
# You can redistribute it and/or modify it under the same term as Ruby.
-
#
-
# See URI for general documentation
-
#
-
-
1
require_relative "rfc2396_parser"
-
1
require_relative "rfc3986_parser"
-
-
1
module URI
-
1
REGEXP = RFC2396_REGEXP
-
1
Parser = RFC2396_Parser
-
1
RFC3986_PARSER = RFC3986_Parser.new
-
-
# URI::Parser.new
-
1
DEFAULT_PARSER = Parser.new
-
1
DEFAULT_PARSER.pattern.each_pair do |sym, str|
-
31
unless REGEXP::PATTERN.const_defined?(sym)
-
25
REGEXP::PATTERN.const_set(sym, str)
-
end
-
end
-
1
DEFAULT_PARSER.regexp.each_pair do |sym, str|
-
17
const_set(sym, str)
-
end
-
-
1
module Util # :nodoc:
-
1
def make_components_hash(klass, array_hash)
-
tmp = {}
-
if array_hash.kind_of?(Array) &&
-
array_hash.size == klass.component.size - 1
-
klass.component[1..-1].each_index do |i|
-
begin
-
tmp[klass.component[i + 1]] = array_hash[i].clone
-
rescue TypeError
-
tmp[klass.component[i + 1]] = array_hash[i]
-
end
-
end
-
-
elsif array_hash.kind_of?(Hash)
-
array_hash.each do |key, value|
-
begin
-
tmp[key] = value.clone
-
rescue TypeError
-
tmp[key] = value
-
end
-
end
-
else
-
raise ArgumentError,
-
"expected Array of or Hash of components of #{klass} (#{klass.component[1..-1].join(', ')})"
-
end
-
tmp[:scheme] = klass.to_s.sub(/\A.*::/, '').downcase
-
-
return tmp
-
end
-
1
module_function :make_components_hash
-
end
-
-
# Module for escaping unsafe characters with codes.
-
1
module Escape
-
#
-
# == Synopsis
-
#
-
# URI.escape(str [, unsafe])
-
#
-
# == Args
-
#
-
# +str+::
-
# String to replaces in.
-
# +unsafe+::
-
# Regexp that matches all symbols that must be replaced with codes.
-
# By default uses <tt>UNSAFE</tt>.
-
# When this argument is a String, it represents a character set.
-
#
-
# == Description
-
#
-
# Escapes the string, replacing all unsafe characters with codes.
-
#
-
# This method is obsolete and should not be used. Instead, use
-
# CGI.escape, URI.encode_www_form or URI.encode_www_form_component
-
# depending on your specific use case.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# enc_uri = URI.escape("http://example.com/?a=\11\15")
-
# # => "http://example.com/?a=%09%0D"
-
#
-
# URI.unescape(enc_uri)
-
# # => "http://example.com/?a=\t\r"
-
#
-
# URI.escape("@?@!", "!?")
-
# # => "@%3F@%21"
-
#
-
1
def escape(*arg)
-
warn "URI.escape is obsolete", uplevel: 1
-
DEFAULT_PARSER.escape(*arg)
-
end
-
1
alias encode escape
-
#
-
# == Synopsis
-
#
-
# URI.unescape(str)
-
#
-
# == Args
-
#
-
# +str+::
-
# String to unescape.
-
#
-
# == Description
-
#
-
# This method is obsolete and should not be used. Instead, use
-
# CGI.unescape, URI.decode_www_form or URI.decode_www_form_component
-
# depending on your specific use case.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# enc_uri = URI.escape("http://example.com/?a=\11\15")
-
# # => "http://example.com/?a=%09%0D"
-
#
-
# URI.unescape(enc_uri)
-
# # => "http://example.com/?a=\t\r"
-
#
-
1
def unescape(*arg)
-
warn "URI.unescape is obsolete", uplevel: 1
-
DEFAULT_PARSER.unescape(*arg)
-
end
-
1
alias decode unescape
-
end # module Escape
-
-
1
extend Escape
-
1
include REGEXP
-
-
1
@@schemes = {}
-
# Returns a Hash of the defined schemes.
-
1
def self.scheme_list
-
2
@@schemes
-
end
-
-
#
-
# Base class for all URI exceptions.
-
#
-
1
class Error < StandardError; end
-
#
-
# Not a URI.
-
#
-
1
class InvalidURIError < Error; end
-
#
-
# Not a URI component.
-
#
-
1
class InvalidComponentError < Error; end
-
#
-
# URI is valid, bad usage is not.
-
#
-
1
class BadURIError < Error; end
-
-
#
-
# == Synopsis
-
#
-
# URI::split(uri)
-
#
-
# == Args
-
#
-
# +uri+::
-
# String with URI.
-
#
-
# == Description
-
#
-
# Splits the string on following parts and returns array with result:
-
#
-
# * Scheme
-
# * Userinfo
-
# * Host
-
# * Port
-
# * Registry
-
# * Path
-
# * Opaque
-
# * Query
-
# * Fragment
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# URI.split("http://www.ruby-lang.org/")
-
# # => ["http", nil, "www.ruby-lang.org", nil, nil, "/", nil, nil, nil]
-
#
-
1
def self.split(uri)
-
26
RFC3986_PARSER.split(uri)
-
end
-
-
#
-
# == Synopsis
-
#
-
# URI::parse(uri_str)
-
#
-
# == Args
-
#
-
# +uri_str+::
-
# String with URI.
-
#
-
# == Description
-
#
-
# Creates one of the URI's subclasses instance from the string.
-
#
-
# == Raises
-
#
-
# URI::InvalidURIError::
-
# Raised if URI given is not a correct one.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://www.ruby-lang.org/")
-
# # => #<URI::HTTP http://www.ruby-lang.org/>
-
# uri.scheme
-
# # => "http"
-
# uri.host
-
# # => "www.ruby-lang.org"
-
#
-
# It's recommended to first ::escape the provided +uri_str+ if there are any
-
# invalid URI characters.
-
#
-
1
def self.parse(uri)
-
2
RFC3986_PARSER.parse(uri)
-
end
-
-
#
-
# == Synopsis
-
#
-
# URI::join(str[, str, ...])
-
#
-
# == Args
-
#
-
# +str+::
-
# String(s) to work with, will be converted to RFC3986 URIs before merging.
-
#
-
# == Description
-
#
-
# Joins URIs.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# URI.join("http://example.com/","main.rbx")
-
# # => #<URI::HTTP http://example.com/main.rbx>
-
#
-
# URI.join('http://example.com', 'foo')
-
# # => #<URI::HTTP http://example.com/foo>
-
#
-
# URI.join('http://example.com', '/foo', '/bar')
-
# # => #<URI::HTTP http://example.com/bar>
-
#
-
# URI.join('http://example.com', '/foo', 'bar')
-
# # => #<URI::HTTP http://example.com/bar>
-
#
-
# URI.join('http://example.com', '/foo/', 'bar')
-
# # => #<URI::HTTP http://example.com/foo/bar>
-
#
-
1
def self.join(*str)
-
RFC3986_PARSER.join(*str)
-
end
-
-
#
-
# == Synopsis
-
#
-
# URI::extract(str[, schemes][,&blk])
-
#
-
# == Args
-
#
-
# +str+::
-
# String to extract URIs from.
-
# +schemes+::
-
# Limit URI matching to specific schemes.
-
#
-
# == Description
-
#
-
# Extracts URIs from a string. If block given, iterates through all matched URIs.
-
# Returns nil if block given or array with matches.
-
#
-
# == Usage
-
#
-
# require "uri"
-
#
-
# URI.extract("text here http://foo.example.org/bla and here mailto:test@example.com and here also.")
-
# # => ["http://foo.example.com/bla", "mailto:test@example.com"]
-
#
-
1
def self.extract(str, schemes = nil, &block)
-
warn "URI.extract is obsolete", uplevel: 1 if $VERBOSE
-
DEFAULT_PARSER.extract(str, schemes, &block)
-
end
-
-
#
-
# == Synopsis
-
#
-
# URI::regexp([match_schemes])
-
#
-
# == Args
-
#
-
# +match_schemes+::
-
# Array of schemes. If given, resulting regexp matches to URIs
-
# whose scheme is one of the match_schemes.
-
#
-
# == Description
-
#
-
# Returns a Regexp object which matches to URI-like strings.
-
# The Regexp object returned by this method includes arbitrary
-
# number of capture group (parentheses). Never rely on it's number.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# # extract first URI from html_string
-
# html_string.slice(URI.regexp)
-
#
-
# # remove ftp URIs
-
# html_string.sub(URI.regexp(['ftp']), '')
-
#
-
# # You should not rely on the number of parentheses
-
# html_string.scan(URI.regexp) do |*matches|
-
# p $&
-
# end
-
#
-
1
def self.regexp(schemes = nil)
-
warn "URI.regexp is obsolete", uplevel: 1 if $VERBOSE
-
DEFAULT_PARSER.make_regexp(schemes)
-
end
-
-
1
TBLENCWWWCOMP_ = {} # :nodoc:
-
1
256.times do |i|
-
256
TBLENCWWWCOMP_[-i.chr] = -('%%%02X' % i)
-
end
-
1
TBLENCWWWCOMP_[' '] = '+'
-
1
TBLENCWWWCOMP_.freeze
-
1
TBLDECWWWCOMP_ = {} # :nodoc:
-
1
256.times do |i|
-
256
h, l = i>>4, i&15
-
256
TBLDECWWWCOMP_[-('%%%X%X' % [h, l])] = -i.chr
-
256
TBLDECWWWCOMP_[-('%%%x%X' % [h, l])] = -i.chr
-
256
TBLDECWWWCOMP_[-('%%%X%x' % [h, l])] = -i.chr
-
256
TBLDECWWWCOMP_[-('%%%x%x' % [h, l])] = -i.chr
-
end
-
1
TBLDECWWWCOMP_['+'] = ' '
-
1
TBLDECWWWCOMP_.freeze
-
-
# Encodes given +str+ to URL-encoded form data.
-
#
-
# This method doesn't convert *, -, ., 0-9, A-Z, _, a-z, but does convert SP
-
# (ASCII space) to + and converts others to %XX.
-
#
-
# If +enc+ is given, convert +str+ to the encoding before percent encoding.
-
#
-
# This is an implementation of
-
# http://www.w3.org/TR/2013/CR-html5-20130806/forms.html#url-encoded-form-data.
-
#
-
# See URI.decode_www_form_component, URI.encode_www_form.
-
1
def self.encode_www_form_component(str, enc=nil)
-
str = str.to_s.dup
-
if str.encoding != Encoding::ASCII_8BIT
-
if enc && enc != Encoding::ASCII_8BIT
-
str.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace)
-
str.encode!(enc, fallback: ->(x){"&##{x.ord};"})
-
end
-
str.force_encoding(Encoding::ASCII_8BIT)
-
end
-
str.gsub!(/[^*\-.0-9A-Z_a-z]/, TBLENCWWWCOMP_)
-
str.force_encoding(Encoding::US_ASCII)
-
end
-
-
# Decodes given +str+ of URL-encoded form data.
-
#
-
# This decodes + to SP.
-
#
-
# See URI.encode_www_form_component, URI.decode_www_form.
-
1
def self.decode_www_form_component(str, enc=Encoding::UTF_8)
-
2
raise ArgumentError, "invalid %-encoding (#{str})" if /%(?!\h\h)/ =~ str
-
2
str.b.gsub(/\+|%\h\h/, TBLDECWWWCOMP_).force_encoding(enc)
-
end
-
-
# Generates URL-encoded form data from given +enum+.
-
#
-
# This generates application/x-www-form-urlencoded data defined in HTML5
-
# from given an Enumerable object.
-
#
-
# This internally uses URI.encode_www_form_component(str).
-
#
-
# This method doesn't convert the encoding of given items, so convert them
-
# before calling this method if you want to send data as other than original
-
# encoding or mixed encoding data. (Strings which are encoded in an HTML5
-
# ASCII incompatible encoding are converted to UTF-8.)
-
#
-
# This method doesn't handle files. When you send a file, use
-
# multipart/form-data.
-
#
-
# This refers http://url.spec.whatwg.org/#concept-urlencoded-serializer
-
#
-
# URI.encode_www_form([["q", "ruby"], ["lang", "en"]])
-
# #=> "q=ruby&lang=en"
-
# URI.encode_www_form("q" => "ruby", "lang" => "en")
-
# #=> "q=ruby&lang=en"
-
# URI.encode_www_form("q" => ["ruby", "perl"], "lang" => "en")
-
# #=> "q=ruby&q=perl&lang=en"
-
# URI.encode_www_form([["q", "ruby"], ["q", "perl"], ["lang", "en"]])
-
# #=> "q=ruby&q=perl&lang=en"
-
#
-
# See URI.encode_www_form_component, URI.decode_www_form.
-
1
def self.encode_www_form(enum, enc=nil)
-
enum.map do |k,v|
-
if v.nil?
-
encode_www_form_component(k, enc)
-
elsif v.respond_to?(:to_ary)
-
v.to_ary.map do |w|
-
str = encode_www_form_component(k, enc)
-
unless w.nil?
-
str << '='
-
str << encode_www_form_component(w, enc)
-
end
-
end.join('&')
-
else
-
str = encode_www_form_component(k, enc)
-
str << '='
-
str << encode_www_form_component(v, enc)
-
end
-
end.join('&')
-
end
-
-
# Decodes URL-encoded form data from given +str+.
-
#
-
# This decodes application/x-www-form-urlencoded data
-
# and returns an array of key-value arrays.
-
#
-
# This refers http://url.spec.whatwg.org/#concept-urlencoded-parser,
-
# so this supports only &-separator, and doesn't support ;-separator.
-
#
-
# ary = URI.decode_www_form("a=1&a=2&b=3")
-
# ary #=> [['a', '1'], ['a', '2'], ['b', '3']]
-
# ary.assoc('a').last #=> '1'
-
# ary.assoc('b').last #=> '3'
-
# ary.rassoc('a').last #=> '2'
-
# Hash[ary] #=> {"a"=>"2", "b"=>"3"}
-
#
-
# See URI.decode_www_form_component, URI.encode_www_form.
-
1
def self.decode_www_form(str, enc=Encoding::UTF_8, separator: '&', use__charset_: false, isindex: false)
-
raise ArgumentError, "the input of #{self.name}.#{__method__} must be ASCII only string" unless str.ascii_only?
-
ary = []
-
return ary if str.empty?
-
enc = Encoding.find(enc)
-
str.b.each_line(separator) do |string|
-
string.chomp!(separator)
-
key, sep, val = string.partition('=')
-
if isindex
-
if sep.empty?
-
val = key
-
key = +''
-
end
-
isindex = false
-
end
-
-
if use__charset_ and key == '_charset_' and e = get_encoding(val)
-
enc = e
-
use__charset_ = false
-
end
-
-
key.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_)
-
if val
-
val.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_)
-
else
-
val = +''
-
end
-
-
ary << [key, val]
-
end
-
ary.each do |k, v|
-
k.force_encoding(enc)
-
k.scrub!
-
v.force_encoding(enc)
-
v.scrub!
-
end
-
ary
-
end
-
-
1
private
-
=begin command for WEB_ENCODINGS_
-
curl https://encoding.spec.whatwg.org/encodings.json|
-
ruby -rjson -e 'H={}
-
h={
-
"shift_jis"=>"Windows-31J",
-
"euc-jp"=>"cp51932",
-
"iso-2022-jp"=>"cp50221",
-
"x-mac-cyrillic"=>"macCyrillic",
-
}
-
JSON($<.read).map{|x|x["encodings"]}.flatten.each{|x|
-
Encoding.find(n=h.fetch(n=x["name"].downcase,n))rescue next
-
x["labels"].each{|y|H[y]=n}
-
}
-
puts "{"
-
H.each{|k,v|puts %[ #{k.dump}=>#{v.dump},]}
-
puts "}"
-
'
-
=end
-
1
WEB_ENCODINGS_ = {
-
"unicode-1-1-utf-8"=>"utf-8",
-
"utf-8"=>"utf-8",
-
"utf8"=>"utf-8",
-
"866"=>"ibm866",
-
"cp866"=>"ibm866",
-
"csibm866"=>"ibm866",
-
"ibm866"=>"ibm866",
-
"csisolatin2"=>"iso-8859-2",
-
"iso-8859-2"=>"iso-8859-2",
-
"iso-ir-101"=>"iso-8859-2",
-
"iso8859-2"=>"iso-8859-2",
-
"iso88592"=>"iso-8859-2",
-
"iso_8859-2"=>"iso-8859-2",
-
"iso_8859-2:1987"=>"iso-8859-2",
-
"l2"=>"iso-8859-2",
-
"latin2"=>"iso-8859-2",
-
"csisolatin3"=>"iso-8859-3",
-
"iso-8859-3"=>"iso-8859-3",
-
"iso-ir-109"=>"iso-8859-3",
-
"iso8859-3"=>"iso-8859-3",
-
"iso88593"=>"iso-8859-3",
-
"iso_8859-3"=>"iso-8859-3",
-
"iso_8859-3:1988"=>"iso-8859-3",
-
"l3"=>"iso-8859-3",
-
"latin3"=>"iso-8859-3",
-
"csisolatin4"=>"iso-8859-4",
-
"iso-8859-4"=>"iso-8859-4",
-
"iso-ir-110"=>"iso-8859-4",
-
"iso8859-4"=>"iso-8859-4",
-
"iso88594"=>"iso-8859-4",
-
"iso_8859-4"=>"iso-8859-4",
-
"iso_8859-4:1988"=>"iso-8859-4",
-
"l4"=>"iso-8859-4",
-
"latin4"=>"iso-8859-4",
-
"csisolatincyrillic"=>"iso-8859-5",
-
"cyrillic"=>"iso-8859-5",
-
"iso-8859-5"=>"iso-8859-5",
-
"iso-ir-144"=>"iso-8859-5",
-
"iso8859-5"=>"iso-8859-5",
-
"iso88595"=>"iso-8859-5",
-
"iso_8859-5"=>"iso-8859-5",
-
"iso_8859-5:1988"=>"iso-8859-5",
-
"arabic"=>"iso-8859-6",
-
"asmo-708"=>"iso-8859-6",
-
"csiso88596e"=>"iso-8859-6",
-
"csiso88596i"=>"iso-8859-6",
-
"csisolatinarabic"=>"iso-8859-6",
-
"ecma-114"=>"iso-8859-6",
-
"iso-8859-6"=>"iso-8859-6",
-
"iso-8859-6-e"=>"iso-8859-6",
-
"iso-8859-6-i"=>"iso-8859-6",
-
"iso-ir-127"=>"iso-8859-6",
-
"iso8859-6"=>"iso-8859-6",
-
"iso88596"=>"iso-8859-6",
-
"iso_8859-6"=>"iso-8859-6",
-
"iso_8859-6:1987"=>"iso-8859-6",
-
"csisolatingreek"=>"iso-8859-7",
-
"ecma-118"=>"iso-8859-7",
-
"elot_928"=>"iso-8859-7",
-
"greek"=>"iso-8859-7",
-
"greek8"=>"iso-8859-7",
-
"iso-8859-7"=>"iso-8859-7",
-
"iso-ir-126"=>"iso-8859-7",
-
"iso8859-7"=>"iso-8859-7",
-
"iso88597"=>"iso-8859-7",
-
"iso_8859-7"=>"iso-8859-7",
-
"iso_8859-7:1987"=>"iso-8859-7",
-
"sun_eu_greek"=>"iso-8859-7",
-
"csiso88598e"=>"iso-8859-8",
-
"csisolatinhebrew"=>"iso-8859-8",
-
"hebrew"=>"iso-8859-8",
-
"iso-8859-8"=>"iso-8859-8",
-
"iso-8859-8-e"=>"iso-8859-8",
-
"iso-ir-138"=>"iso-8859-8",
-
"iso8859-8"=>"iso-8859-8",
-
"iso88598"=>"iso-8859-8",
-
"iso_8859-8"=>"iso-8859-8",
-
"iso_8859-8:1988"=>"iso-8859-8",
-
"visual"=>"iso-8859-8",
-
"csisolatin6"=>"iso-8859-10",
-
"iso-8859-10"=>"iso-8859-10",
-
"iso-ir-157"=>"iso-8859-10",
-
"iso8859-10"=>"iso-8859-10",
-
"iso885910"=>"iso-8859-10",
-
"l6"=>"iso-8859-10",
-
"latin6"=>"iso-8859-10",
-
"iso-8859-13"=>"iso-8859-13",
-
"iso8859-13"=>"iso-8859-13",
-
"iso885913"=>"iso-8859-13",
-
"iso-8859-14"=>"iso-8859-14",
-
"iso8859-14"=>"iso-8859-14",
-
"iso885914"=>"iso-8859-14",
-
"csisolatin9"=>"iso-8859-15",
-
"iso-8859-15"=>"iso-8859-15",
-
"iso8859-15"=>"iso-8859-15",
-
"iso885915"=>"iso-8859-15",
-
"iso_8859-15"=>"iso-8859-15",
-
"l9"=>"iso-8859-15",
-
"iso-8859-16"=>"iso-8859-16",
-
"cskoi8r"=>"koi8-r",
-
"koi"=>"koi8-r",
-
"koi8"=>"koi8-r",
-
"koi8-r"=>"koi8-r",
-
"koi8_r"=>"koi8-r",
-
"koi8-ru"=>"koi8-u",
-
"koi8-u"=>"koi8-u",
-
"dos-874"=>"windows-874",
-
"iso-8859-11"=>"windows-874",
-
"iso8859-11"=>"windows-874",
-
"iso885911"=>"windows-874",
-
"tis-620"=>"windows-874",
-
"windows-874"=>"windows-874",
-
"cp1250"=>"windows-1250",
-
"windows-1250"=>"windows-1250",
-
"x-cp1250"=>"windows-1250",
-
"cp1251"=>"windows-1251",
-
"windows-1251"=>"windows-1251",
-
"x-cp1251"=>"windows-1251",
-
"ansi_x3.4-1968"=>"windows-1252",
-
"ascii"=>"windows-1252",
-
"cp1252"=>"windows-1252",
-
"cp819"=>"windows-1252",
-
"csisolatin1"=>"windows-1252",
-
"ibm819"=>"windows-1252",
-
"iso-8859-1"=>"windows-1252",
-
"iso-ir-100"=>"windows-1252",
-
"iso8859-1"=>"windows-1252",
-
"iso88591"=>"windows-1252",
-
"iso_8859-1"=>"windows-1252",
-
"iso_8859-1:1987"=>"windows-1252",
-
"l1"=>"windows-1252",
-
"latin1"=>"windows-1252",
-
"us-ascii"=>"windows-1252",
-
"windows-1252"=>"windows-1252",
-
"x-cp1252"=>"windows-1252",
-
"cp1253"=>"windows-1253",
-
"windows-1253"=>"windows-1253",
-
"x-cp1253"=>"windows-1253",
-
"cp1254"=>"windows-1254",
-
"csisolatin5"=>"windows-1254",
-
"iso-8859-9"=>"windows-1254",
-
"iso-ir-148"=>"windows-1254",
-
"iso8859-9"=>"windows-1254",
-
"iso88599"=>"windows-1254",
-
"iso_8859-9"=>"windows-1254",
-
"iso_8859-9:1989"=>"windows-1254",
-
"l5"=>"windows-1254",
-
"latin5"=>"windows-1254",
-
"windows-1254"=>"windows-1254",
-
"x-cp1254"=>"windows-1254",
-
"cp1255"=>"windows-1255",
-
"windows-1255"=>"windows-1255",
-
"x-cp1255"=>"windows-1255",
-
"cp1256"=>"windows-1256",
-
"windows-1256"=>"windows-1256",
-
"x-cp1256"=>"windows-1256",
-
"cp1257"=>"windows-1257",
-
"windows-1257"=>"windows-1257",
-
"x-cp1257"=>"windows-1257",
-
"cp1258"=>"windows-1258",
-
"windows-1258"=>"windows-1258",
-
"x-cp1258"=>"windows-1258",
-
"x-mac-cyrillic"=>"macCyrillic",
-
"x-mac-ukrainian"=>"macCyrillic",
-
"chinese"=>"gbk",
-
"csgb2312"=>"gbk",
-
"csiso58gb231280"=>"gbk",
-
"gb2312"=>"gbk",
-
"gb_2312"=>"gbk",
-
"gb_2312-80"=>"gbk",
-
"gbk"=>"gbk",
-
"iso-ir-58"=>"gbk",
-
"x-gbk"=>"gbk",
-
"gb18030"=>"gb18030",
-
"big5"=>"big5",
-
"big5-hkscs"=>"big5",
-
"cn-big5"=>"big5",
-
"csbig5"=>"big5",
-
"x-x-big5"=>"big5",
-
"cseucpkdfmtjapanese"=>"cp51932",
-
"euc-jp"=>"cp51932",
-
"x-euc-jp"=>"cp51932",
-
"csiso2022jp"=>"cp50221",
-
"iso-2022-jp"=>"cp50221",
-
"csshiftjis"=>"Windows-31J",
-
"ms932"=>"Windows-31J",
-
"ms_kanji"=>"Windows-31J",
-
"shift-jis"=>"Windows-31J",
-
"shift_jis"=>"Windows-31J",
-
"sjis"=>"Windows-31J",
-
"windows-31j"=>"Windows-31J",
-
"x-sjis"=>"Windows-31J",
-
"cseuckr"=>"euc-kr",
-
"csksc56011987"=>"euc-kr",
-
"euc-kr"=>"euc-kr",
-
"iso-ir-149"=>"euc-kr",
-
"korean"=>"euc-kr",
-
"ks_c_5601-1987"=>"euc-kr",
-
"ks_c_5601-1989"=>"euc-kr",
-
"ksc5601"=>"euc-kr",
-
"ksc_5601"=>"euc-kr",
-
"windows-949"=>"euc-kr",
-
"utf-16be"=>"utf-16be",
-
"utf-16"=>"utf-16le",
-
"utf-16le"=>"utf-16le",
-
} # :nodoc:
-
-
# :nodoc:
-
# return encoding or nil
-
# http://encoding.spec.whatwg.org/#concept-encoding-get
-
1
def self.get_encoding(label)
-
Encoding.find(WEB_ENCODINGS_[label.to_str.strip.downcase]) rescue nil
-
end
-
end # module URI
-
-
1
module Kernel
-
-
#
-
# Returns +uri+ converted to an URI object.
-
#
-
1
def URI(uri)
-
if uri.is_a?(URI::Generic)
-
uri
-
elsif uri = String.try_convert(uri)
-
URI.parse(uri)
-
else
-
raise ArgumentError,
-
"bad argument (expected URI object or URI string)"
-
end
-
end
-
1
module_function :URI
-
end
-
# frozen_string_literal: true
-
-
1
require_relative 'generic'
-
-
1
module URI
-
-
#
-
# The "file" URI is defined by RFC8089.
-
#
-
1
class File < Generic
-
# A Default port of nil for URI::File.
-
1
DEFAULT_PORT = nil
-
-
#
-
# An Array of the available components for URI::File.
-
#
-
1
COMPONENT = [
-
:scheme,
-
:host,
-
:path
-
].freeze
-
-
#
-
# == Description
-
#
-
# Creates a new URI::File object from components, with syntax checking.
-
#
-
# The components accepted are +host+ and +path+.
-
#
-
# The components should be provided either as an Array, or as a Hash
-
# with keys formed by preceding the component names with a colon.
-
#
-
# If an Array is used, the components must be passed in the
-
# order <code>[host, path]</code>.
-
#
-
# Examples:
-
#
-
# require 'uri'
-
#
-
# uri1 = URI::File.build(['host.example.com', '/path/file.zip'])
-
# uri1.to_s # => "file://host.example.com/path/file.zip"
-
#
-
# uri2 = URI::File.build({:host => 'host.example.com',
-
# :path => '/ruby/src'})
-
# uri2.to_s # => "file://host.example.com/ruby/src"
-
#
-
1
def self.build(args)
-
tmp = Util::make_components_hash(self, args)
-
super(tmp)
-
end
-
-
# Protected setter for the host component +v+.
-
#
-
# See also URI::Generic.host=.
-
#
-
1
def set_host(v)
-
v = "" if v.nil? || v == "localhost"
-
@host = v
-
end
-
-
# do nothing
-
1
def set_port(v)
-
end
-
-
# raise InvalidURIError
-
1
def check_userinfo(user)
-
raise URI::InvalidURIError, "can not set userinfo for file URI"
-
end
-
-
# raise InvalidURIError
-
1
def check_user(user)
-
raise URI::InvalidURIError, "can not set user for file URI"
-
end
-
-
# raise InvalidURIError
-
1
def check_password(user)
-
raise URI::InvalidURIError, "can not set password for file URI"
-
end
-
-
# do nothing
-
1
def set_userinfo(v)
-
end
-
-
# do nothing
-
1
def set_user(v)
-
end
-
-
# do nothing
-
1
def set_password(v)
-
end
-
end
-
-
1
@@schemes['FILE'] = File
-
end
-
# frozen_string_literal: false
-
# = uri/ftp.rb
-
#
-
# Author:: Akira Yamada <akira@ruby-lang.org>
-
# License:: You can redistribute it and/or modify it under the same term as Ruby.
-
# Revision:: $Id$
-
#
-
# See URI for general documentation
-
#
-
-
1
require_relative 'generic'
-
-
1
module URI
-
-
#
-
# FTP URI syntax is defined by RFC1738 section 3.2.
-
#
-
# This class will be redesigned because of difference of implementations;
-
# the structure of its path. draft-hoffman-ftp-uri-04 is a draft but it
-
# is a good summary about the de facto spec.
-
# http://tools.ietf.org/html/draft-hoffman-ftp-uri-04
-
#
-
1
class FTP < Generic
-
# A Default port of 21 for URI::FTP.
-
1
DEFAULT_PORT = 21
-
-
#
-
# An Array of the available components for URI::FTP.
-
#
-
1
COMPONENT = [
-
:scheme,
-
:userinfo, :host, :port,
-
:path, :typecode
-
].freeze
-
-
#
-
# Typecode is "a", "i", or "d".
-
#
-
# * "a" indicates a text file (the FTP command was ASCII)
-
# * "i" indicates a binary file (FTP command IMAGE)
-
# * "d" indicates the contents of a directory should be displayed
-
#
-
1
TYPECODE = ['a', 'i', 'd'].freeze
-
-
# Typecode prefix ";type=".
-
1
TYPECODE_PREFIX = ';type='.freeze
-
-
1
def self.new2(user, password, host, port, path,
-
typecode = nil, arg_check = true) # :nodoc:
-
# Do not use this method! Not tested. [Bug #7301]
-
# This methods remains just for compatibility,
-
# Keep it undocumented until the active maintainer is assigned.
-
typecode = nil if typecode.size == 0
-
if typecode && !TYPECODE.include?(typecode)
-
raise ArgumentError,
-
"bad typecode is specified: #{typecode}"
-
end
-
-
# do escape
-
-
self.new('ftp',
-
[user, password],
-
host, port, nil,
-
typecode ? path + TYPECODE_PREFIX + typecode : path,
-
nil, nil, nil, arg_check)
-
end
-
-
#
-
# == Description
-
#
-
# Creates a new URI::FTP object from components, with syntax checking.
-
#
-
# The components accepted are +userinfo+, +host+, +port+, +path+, and
-
# +typecode+.
-
#
-
# The components should be provided either as an Array, or as a Hash
-
# with keys formed by preceding the component names with a colon.
-
#
-
# If an Array is used, the components must be passed in the
-
# order <code>[userinfo, host, port, path, typecode]</code>.
-
#
-
# If the path supplied is absolute, it will be escaped in order to
-
# make it absolute in the URI.
-
#
-
# Examples:
-
#
-
# require 'uri'
-
#
-
# uri1 = URI::FTP.build(['user:password', 'ftp.example.com', nil,
-
# '/path/file.zip', 'i'])
-
# uri1.to_s # => "ftp://user:password@ftp.example.com/%2Fpath/file.zip;type=i"
-
#
-
# uri2 = URI::FTP.build({:host => 'ftp.example.com',
-
# :path => 'ruby/src'})
-
# uri2.to_s # => "ftp://ftp.example.com/ruby/src"
-
#
-
1
def self.build(args)
-
-
# Fix the incoming path to be generic URL syntax
-
# FTP path -> URL path
-
# foo/bar /foo/bar
-
# /foo/bar /%2Ffoo/bar
-
#
-
if args.kind_of?(Array)
-
args[3] = '/' + args[3].sub(/^\//, '%2F')
-
else
-
args[:path] = '/' + args[:path].sub(/^\//, '%2F')
-
end
-
-
tmp = Util::make_components_hash(self, args)
-
-
if tmp[:typecode]
-
if tmp[:typecode].size == 1
-
tmp[:typecode] = TYPECODE_PREFIX + tmp[:typecode]
-
end
-
tmp[:path] << tmp[:typecode]
-
end
-
-
return super(tmp)
-
end
-
-
#
-
# == Description
-
#
-
# Creates a new URI::FTP object from generic URL components with no
-
# syntax checking.
-
#
-
# Unlike build(), this method does not escape the path component as
-
# required by RFC1738; instead it is treated as per RFC2396.
-
#
-
# Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+,
-
# +opaque+, +query+, and +fragment+, in that order.
-
#
-
1
def initialize(scheme,
-
userinfo, host, port, registry,
-
path, opaque,
-
query,
-
fragment,
-
parser = nil,
-
arg_check = false)
-
raise InvalidURIError unless path
-
path = path.sub(/^\//,'')
-
path.sub!(/^%2F/,'/')
-
super(scheme, userinfo, host, port, registry, path, opaque,
-
query, fragment, parser, arg_check)
-
@typecode = nil
-
if tmp = @path.index(TYPECODE_PREFIX)
-
typecode = @path[tmp + TYPECODE_PREFIX.size..-1]
-
@path = @path[0..tmp - 1]
-
-
if arg_check
-
self.typecode = typecode
-
else
-
self.set_typecode(typecode)
-
end
-
end
-
end
-
-
# typecode accessor.
-
#
-
# See URI::FTP::COMPONENT.
-
1
attr_reader :typecode
-
-
# Validates typecode +v+,
-
# returns +true+ or +false+.
-
#
-
1
def check_typecode(v)
-
if TYPECODE.include?(v)
-
return true
-
else
-
raise InvalidComponentError,
-
"bad typecode(expected #{TYPECODE.join(', ')}): #{v}"
-
end
-
end
-
1
private :check_typecode
-
-
# Private setter for the typecode +v+.
-
#
-
# See also URI::FTP.typecode=.
-
#
-
1
def set_typecode(v)
-
@typecode = v
-
end
-
1
protected :set_typecode
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the typecode +v+
-
# (with validation).
-
#
-
# See also URI::FTP.check_typecode.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("ftp://john@ftp.example.com/my_file.img")
-
# #=> #<URI::FTP ftp://john@ftp.example.com/my_file.img>
-
# uri.typecode = "i"
-
# uri
-
# #=> #<URI::FTP ftp://john@ftp.example.com/my_file.img;type=i>
-
#
-
1
def typecode=(typecode)
-
check_typecode(typecode)
-
set_typecode(typecode)
-
typecode
-
end
-
-
1
def merge(oth) # :nodoc:
-
tmp = super(oth)
-
if self != tmp
-
tmp.set_typecode(oth.typecode)
-
end
-
-
return tmp
-
end
-
-
# Returns the path from an FTP URI.
-
#
-
# RFC 1738 specifically states that the path for an FTP URI does not
-
# include the / which separates the URI path from the URI host. Example:
-
#
-
# <code>ftp://ftp.example.com/pub/ruby</code>
-
#
-
# The above URI indicates that the client should connect to
-
# ftp.example.com then cd to pub/ruby from the initial login directory.
-
#
-
# If you want to cd to an absolute directory, you must include an
-
# escaped / (%2F) in the path. Example:
-
#
-
# <code>ftp://ftp.example.com/%2Fpub/ruby</code>
-
#
-
# This method will then return "/pub/ruby".
-
#
-
1
def path
-
return @path.sub(/^\//,'').sub(/^%2F/,'/')
-
end
-
-
# Private setter for the path of the URI::FTP.
-
1
def set_path(v)
-
super("/" + v.sub(/^\//, "%2F"))
-
end
-
1
protected :set_path
-
-
# Returns a String representation of the URI::FTP.
-
1
def to_s
-
save_path = nil
-
if @typecode
-
save_path = @path
-
@path = @path + TYPECODE_PREFIX + @typecode
-
end
-
str = super
-
if @typecode
-
@path = save_path
-
end
-
-
return str
-
end
-
end
-
1
@@schemes['FTP'] = FTP
-
end
-
# frozen_string_literal: true
-
-
# = uri/generic.rb
-
#
-
# Author:: Akira Yamada <akira@ruby-lang.org>
-
# License:: You can redistribute it and/or modify it under the same term as Ruby.
-
# Revision:: $Id$
-
#
-
# See URI for general documentation
-
#
-
-
1
require_relative 'common'
-
1
autoload :IPSocket, 'socket'
-
1
autoload :IPAddr, 'ipaddr'
-
-
1
module URI
-
-
#
-
# Base class for all URI classes.
-
# Implements generic URI syntax as per RFC 2396.
-
#
-
1
class Generic
-
1
include URI
-
-
#
-
# A Default port of nil for URI::Generic.
-
#
-
1
DEFAULT_PORT = nil
-
-
#
-
# Returns default port.
-
#
-
1
def self.default_port
-
6
self::DEFAULT_PORT
-
end
-
-
#
-
# Returns default port.
-
#
-
1
def default_port
-
6
self.class.default_port
-
end
-
-
#
-
# An Array of the available components for URI::Generic.
-
#
-
1
COMPONENT = [
-
:scheme,
-
:userinfo, :host, :port, :registry,
-
:path, :opaque,
-
:query,
-
:fragment
-
].freeze
-
-
#
-
# Components of the URI in the order.
-
#
-
1
def self.component
-
self::COMPONENT
-
end
-
-
1
USE_REGISTRY = false # :nodoc:
-
-
1
def self.use_registry # :nodoc:
-
self::USE_REGISTRY
-
end
-
-
#
-
# == Synopsis
-
#
-
# See ::new.
-
#
-
# == Description
-
#
-
# At first, tries to create a new URI::Generic instance using
-
# URI::Generic::build. But, if exception URI::InvalidComponentError is raised,
-
# then it does URI::Escape.escape all URI components and tries again.
-
#
-
1
def self.build2(args)
-
begin
-
return self.build(args)
-
rescue InvalidComponentError
-
if args.kind_of?(Array)
-
return self.build(args.collect{|x|
-
if x.is_a?(String)
-
DEFAULT_PARSER.escape(x)
-
else
-
x
-
end
-
})
-
elsif args.kind_of?(Hash)
-
tmp = {}
-
args.each do |key, value|
-
tmp[key] = if value
-
DEFAULT_PARSER.escape(value)
-
else
-
value
-
end
-
end
-
return self.build(tmp)
-
end
-
end
-
end
-
-
#
-
# == Synopsis
-
#
-
# See ::new.
-
#
-
# == Description
-
#
-
# Creates a new URI::Generic instance from components of URI::Generic
-
# with check. Components are: scheme, userinfo, host, port, registry, path,
-
# opaque, query, and fragment. You can provide arguments either by an Array or a Hash.
-
# See ::new for hash keys to use or for order of array items.
-
#
-
1
def self.build(args)
-
if args.kind_of?(Array) &&
-
args.size == ::URI::Generic::COMPONENT.size
-
tmp = args.dup
-
elsif args.kind_of?(Hash)
-
tmp = ::URI::Generic::COMPONENT.collect do |c|
-
if args.include?(c)
-
args[c]
-
else
-
nil
-
end
-
end
-
else
-
component = self.class.component rescue ::URI::Generic::COMPONENT
-
raise ArgumentError,
-
"expected Array of or Hash of components of #{self.class} (#{component.join(', ')})"
-
end
-
-
tmp << nil
-
tmp << true
-
return self.new(*tmp)
-
end
-
-
#
-
# == Args
-
#
-
# +scheme+::
-
# Protocol scheme, i.e. 'http','ftp','mailto' and so on.
-
# +userinfo+::
-
# User name and password, i.e. 'sdmitry:bla'.
-
# +host+::
-
# Server host name.
-
# +port+::
-
# Server port.
-
# +registry+::
-
# Registry of naming authorities.
-
# +path+::
-
# Path on server.
-
# +opaque+::
-
# Opaque part.
-
# +query+::
-
# Query data.
-
# +fragment+::
-
# Part of the URI after '#' character.
-
# +parser+::
-
# Parser for internal use [URI::DEFAULT_PARSER by default].
-
# +arg_check+::
-
# Check arguments [false by default].
-
#
-
# == Description
-
#
-
# Creates a new URI::Generic instance from ``generic'' components without check.
-
#
-
1
def initialize(scheme,
-
userinfo, host, port, registry,
-
path, opaque,
-
query,
-
fragment,
-
parser = DEFAULT_PARSER,
-
arg_check = false)
-
4
@scheme = nil
-
4
@user = nil
-
4
@password = nil
-
4
@host = nil
-
4
@port = nil
-
4
@path = nil
-
4
@query = nil
-
4
@opaque = nil
-
4
@fragment = nil
-
4
@parser = parser == DEFAULT_PARSER ? nil : parser
-
-
4
if arg_check
-
self.scheme = scheme
-
self.userinfo = userinfo
-
self.hostname = host
-
self.port = port
-
self.path = path
-
self.query = query
-
self.opaque = opaque
-
self.fragment = fragment
-
else
-
4
self.set_scheme(scheme)
-
4
self.set_userinfo(userinfo)
-
4
self.set_host(host)
-
4
self.set_port(port)
-
4
self.set_path(path)
-
4
self.query = query
-
4
self.set_opaque(opaque)
-
4
self.fragment=(fragment)
-
end
-
4
if registry
-
raise InvalidURIError,
-
"the scheme #{@scheme} does not accept registry part: #{registry} (or bad hostname?)"
-
end
-
-
4
@scheme&.freeze
-
4
self.set_path('') if !@path && !@opaque # (see RFC2396 Section 5.2)
-
4
self.set_port(self.default_port) if self.default_port && !@port
-
end
-
-
#
-
# Returns the scheme component of the URI.
-
#
-
# URI("http://foo/bar/baz").scheme #=> "http"
-
#
-
1
attr_reader :scheme
-
-
# Returns the host component of the URI.
-
#
-
# URI("http://foo/bar/baz").host #=> "foo"
-
#
-
# It returns nil if no host component exists.
-
#
-
# URI("mailto:foo@example.org").host #=> nil
-
#
-
# The component does not contain the port number.
-
#
-
# URI("http://foo:8080/bar/baz").host #=> "foo"
-
#
-
# Since IPv6 addresses are wrapped with brackets in URIs,
-
# this method returns IPv6 addresses wrapped with brackets.
-
# This form is not appropriate to pass to socket methods such as TCPSocket.open.
-
# If unwrapped host names are required, use the #hostname method.
-
#
-
# URI("http://[::1]/bar/baz").host #=> "[::1]"
-
# URI("http://[::1]/bar/baz").hostname #=> "::1"
-
#
-
1
attr_reader :host
-
-
# Returns the port component of the URI.
-
#
-
# URI("http://foo/bar/baz").port #=> 80
-
# URI("http://foo:8080/bar/baz").port #=> 8080
-
#
-
1
attr_reader :port
-
-
1
def registry # :nodoc:
-
nil
-
end
-
-
# Returns the path component of the URI.
-
#
-
# URI("http://foo/bar/baz").path #=> "/bar/baz"
-
#
-
1
attr_reader :path
-
-
# Returns the query component of the URI.
-
#
-
# URI("http://foo/bar/baz?search=FooBar").query #=> "search=FooBar"
-
#
-
1
attr_reader :query
-
-
# Returns the opaque part of the URI.
-
#
-
# URI("mailto:foo@example.org").opaque #=> "foo@example.org"
-
# URI("http://foo/bar/baz").opaque #=> nil
-
#
-
# The portion of the path that does not make use of the slash '/'.
-
# The path typically refers to an absolute path or an opaque part.
-
# (See RFC2396 Section 3 and 5.2.)
-
#
-
1
attr_reader :opaque
-
-
# Returns the fragment component of the URI.
-
#
-
# URI("http://foo/bar/baz?search=FooBar#ponies").fragment #=> "ponies"
-
#
-
1
attr_reader :fragment
-
-
# Returns the parser to be used.
-
#
-
# Unless a URI::Parser is defined, DEFAULT_PARSER is used.
-
#
-
1
def parser
-
2
if !defined?(@parser) || !@parser
-
DEFAULT_PARSER
-
else
-
2
@parser || DEFAULT_PARSER
-
end
-
end
-
-
# Replaces self by other URI object.
-
#
-
1
def replace!(oth)
-
if self.class != oth.class
-
raise ArgumentError, "expected #{self.class} object"
-
end
-
-
component.each do |c|
-
self.__send__("#{c}=", oth.__send__(c))
-
end
-
end
-
1
private :replace!
-
-
#
-
# Components of the URI in the order.
-
#
-
1
def component
-
self.class.component
-
end
-
-
#
-
# Checks the scheme +v+ component against the URI::Parser Regexp for :SCHEME.
-
#
-
1
def check_scheme(v)
-
if v && parser.regexp[:SCHEME] !~ v
-
raise InvalidComponentError,
-
"bad component(expected scheme component): #{v}"
-
end
-
-
return true
-
end
-
1
private :check_scheme
-
-
# Protected setter for the scheme component +v+.
-
#
-
# See also URI::Generic.scheme=.
-
#
-
1
def set_scheme(v)
-
4
@scheme = v&.downcase
-
end
-
1
protected :set_scheme
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the scheme component +v+
-
# (with validation).
-
#
-
# See also URI::Generic.check_scheme.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com")
-
# uri.scheme = "https"
-
# uri.to_s #=> "https://my.example.com"
-
#
-
1
def scheme=(v)
-
check_scheme(v)
-
set_scheme(v)
-
v
-
end
-
-
#
-
# Checks the +user+ and +password+.
-
#
-
# If +password+ is not provided, then +user+ is
-
# split, using URI::Generic.split_userinfo, to
-
# pull +user+ and +password.
-
#
-
# See also URI::Generic.check_user, URI::Generic.check_password.
-
#
-
1
def check_userinfo(user, password = nil)
-
if !password
-
user, password = split_userinfo(user)
-
end
-
check_user(user)
-
check_password(password, user)
-
-
return true
-
end
-
1
private :check_userinfo
-
-
#
-
# Checks the user +v+ component for RFC2396 compliance
-
# and against the URI::Parser Regexp for :USERINFO.
-
#
-
# Can not have a registry or opaque component defined,
-
# with a user component defined.
-
#
-
1
def check_user(v)
-
if @opaque
-
raise InvalidURIError,
-
"can not set user with opaque"
-
end
-
-
return v unless v
-
-
if parser.regexp[:USERINFO] !~ v
-
raise InvalidComponentError,
-
"bad component(expected userinfo component or user component): #{v}"
-
end
-
-
return true
-
end
-
1
private :check_user
-
-
#
-
# Checks the password +v+ component for RFC2396 compliance
-
# and against the URI::Parser Regexp for :USERINFO.
-
#
-
# Can not have a registry or opaque component defined,
-
# with a user component defined.
-
#
-
1
def check_password(v, user = @user)
-
if @opaque
-
raise InvalidURIError,
-
"can not set password with opaque"
-
end
-
return v unless v
-
-
if !user
-
raise InvalidURIError,
-
"password component depends user component"
-
end
-
-
if parser.regexp[:USERINFO] !~ v
-
raise InvalidComponentError,
-
"bad password component"
-
end
-
-
return true
-
end
-
1
private :check_password
-
-
#
-
# Sets userinfo, argument is string like 'name:pass'.
-
#
-
1
def userinfo=(userinfo)
-
if userinfo.nil?
-
return nil
-
end
-
check_userinfo(*userinfo)
-
set_userinfo(*userinfo)
-
# returns userinfo
-
end
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the +user+ component
-
# (with validation).
-
#
-
# See also URI::Generic.check_user.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://john:S3nsit1ve@my.example.com")
-
# uri.user = "sam"
-
# uri.to_s #=> "http://sam:V3ry_S3nsit1ve@my.example.com"
-
#
-
1
def user=(user)
-
check_user(user)
-
set_user(user)
-
# returns user
-
end
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the +password+ component
-
# (with validation).
-
#
-
# See also URI::Generic.check_password.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://john:S3nsit1ve@my.example.com")
-
# uri.password = "V3ry_S3nsit1ve"
-
# uri.to_s #=> "http://john:V3ry_S3nsit1ve@my.example.com"
-
#
-
1
def password=(password)
-
check_password(password)
-
set_password(password)
-
# returns password
-
end
-
-
# Protected setter for the +user+ component, and +password+ if available
-
# (with validation).
-
#
-
# See also URI::Generic.userinfo=.
-
#
-
1
def set_userinfo(user, password = nil)
-
4
unless password
-
4
user, password = split_userinfo(user)
-
end
-
4
@user = user
-
4
@password = password if password
-
-
4
[@user, @password]
-
end
-
1
protected :set_userinfo
-
-
# Protected setter for the user component +v+.
-
#
-
# See also URI::Generic.user=.
-
#
-
1
def set_user(v)
-
set_userinfo(v, @password)
-
v
-
end
-
1
protected :set_user
-
-
# Protected setter for the password component +v+.
-
#
-
# See also URI::Generic.password=.
-
#
-
1
def set_password(v)
-
@password = v
-
# returns v
-
end
-
1
protected :set_password
-
-
# Returns the userinfo +ui+ as <code>[user, password]</code>
-
# if properly formatted as 'user:password'.
-
1
def split_userinfo(ui)
-
4
return nil, nil unless ui
-
user, password = ui.split(':', 2)
-
-
return user, password
-
end
-
1
private :split_userinfo
-
-
# Escapes 'user:password' +v+ based on RFC 1738 section 3.1.
-
1
def escape_userpass(v)
-
parser.escape(v, /[@:\/]/o) # RFC 1738 section 3.1 #/
-
end
-
1
private :escape_userpass
-
-
# Returns the userinfo, either as 'user' or 'user:password'.
-
1
def userinfo
-
2
if @user.nil?
-
nil
-
elsif @password.nil?
-
@user
-
else
-
@user + ':' + @password
-
end
-
end
-
-
# Returns the user component.
-
1
def user
-
@user
-
end
-
-
# Returns the password component.
-
1
def password
-
@password
-
end
-
-
#
-
# Checks the host +v+ component for RFC2396 compliance
-
# and against the URI::Parser Regexp for :HOST.
-
#
-
# Can not have a registry or opaque component defined,
-
# with a host component defined.
-
#
-
1
def check_host(v)
-
2
return v unless v
-
-
2
if @opaque
-
raise InvalidURIError,
-
"can not set host with registry or opaque"
-
2
elsif parser.regexp[:HOST] !~ v
-
raise InvalidComponentError,
-
"bad component(expected host component): #{v}"
-
end
-
-
2
return true
-
end
-
1
private :check_host
-
-
# Protected setter for the host component +v+.
-
#
-
# See also URI::Generic.host=.
-
#
-
1
def set_host(v)
-
6
@host = v
-
end
-
1
protected :set_host
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the host component +v+
-
# (with validation).
-
#
-
# See also URI::Generic.check_host.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com")
-
# uri.host = "foo.com"
-
# uri.to_s #=> "http://foo.com"
-
#
-
1
def host=(v)
-
2
check_host(v)
-
2
set_host(v)
-
2
v
-
end
-
-
# Extract the host part of the URI and unwrap brackets for IPv6 addresses.
-
#
-
# This method is the same as URI::Generic#host except
-
# brackets for IPv6 (and future IP) addresses are removed.
-
#
-
# uri = URI("http://[::1]/bar")
-
# uri.hostname #=> "::1"
-
# uri.host #=> "[::1]"
-
#
-
1
def hostname
-
v = self.host
-
/\A\[(.*)\]\z/ =~ v ? $1 : v
-
end
-
-
# Sets the host part of the URI as the argument with brackets for IPv6 addresses.
-
#
-
# This method is the same as URI::Generic#host= except
-
# the argument can be a bare IPv6 address.
-
#
-
# uri = URI("http://foo/bar")
-
# uri.hostname = "::1"
-
# uri.to_s #=> "http://[::1]/bar"
-
#
-
# If the argument seems to be an IPv6 address,
-
# it is wrapped with brackets.
-
#
-
1
def hostname=(v)
-
v = "[#{v}]" if /\A\[.*\]\z/ !~ v && /:/ =~ v
-
self.host = v
-
end
-
-
#
-
# Checks the port +v+ component for RFC2396 compliance
-
# and against the URI::Parser Regexp for :PORT.
-
#
-
# Can not have a registry or opaque component defined,
-
# with a port component defined.
-
#
-
1
def check_port(v)
-
return v unless v
-
-
if @opaque
-
raise InvalidURIError,
-
"can not set port with registry or opaque"
-
elsif !v.kind_of?(Integer) && parser.regexp[:PORT] !~ v
-
raise InvalidComponentError,
-
"bad component(expected port component): #{v.inspect}"
-
end
-
-
return true
-
end
-
1
private :check_port
-
-
# Protected setter for the port component +v+.
-
#
-
# See also URI::Generic.port=.
-
#
-
1
def set_port(v)
-
4
v = v.empty? ? nil : v.to_i unless !v || v.kind_of?(Integer)
-
4
@port = v
-
end
-
1
protected :set_port
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the port component +v+
-
# (with validation).
-
#
-
# See also URI::Generic.check_port.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com")
-
# uri.port = 8080
-
# uri.to_s #=> "http://my.example.com:8080"
-
#
-
1
def port=(v)
-
check_port(v)
-
set_port(v)
-
port
-
end
-
-
1
def check_registry(v) # :nodoc:
-
raise InvalidURIError, "can not set registry"
-
end
-
1
private :check_registry
-
-
1
def set_registry(v) #:nodoc:
-
raise InvalidURIError, "can not set registry"
-
end
-
1
protected :set_registry
-
-
1
def registry=(v)
-
raise InvalidURIError, "can not set registry"
-
end
-
-
#
-
# Checks the path +v+ component for RFC2396 compliance
-
# and against the URI::Parser Regexp
-
# for :ABS_PATH and :REL_PATH.
-
#
-
# Can not have a opaque component defined,
-
# with a path component defined.
-
#
-
1
def check_path(v)
-
# raise if both hier and opaque are not nil, because:
-
# absoluteURI = scheme ":" ( hier_part | opaque_part )
-
# hier_part = ( net_path | abs_path ) [ "?" query ]
-
if v && @opaque
-
raise InvalidURIError,
-
"path conflicts with opaque"
-
end
-
-
# If scheme is ftp, path may be relative.
-
# See RFC 1738 section 3.2.2, and RFC 2396.
-
if @scheme && @scheme != "ftp"
-
if v && v != '' && parser.regexp[:ABS_PATH] !~ v
-
raise InvalidComponentError,
-
"bad component(expected absolute path component): #{v}"
-
end
-
else
-
if v && v != '' && parser.regexp[:ABS_PATH] !~ v &&
-
parser.regexp[:REL_PATH] !~ v
-
raise InvalidComponentError,
-
"bad component(expected relative path component): #{v}"
-
end
-
end
-
-
return true
-
end
-
1
private :check_path
-
-
# Protected setter for the path component +v+.
-
#
-
# See also URI::Generic.path=.
-
#
-
1
def set_path(v)
-
4
@path = v
-
end
-
1
protected :set_path
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the path component +v+
-
# (with validation).
-
#
-
# See also URI::Generic.check_path.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com/pub/files")
-
# uri.path = "/faq/"
-
# uri.to_s #=> "http://my.example.com/faq/"
-
#
-
1
def path=(v)
-
check_path(v)
-
set_path(v)
-
v
-
end
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the query component +v+.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com/?id=25")
-
# uri.query = "id=1"
-
# uri.to_s #=> "http://my.example.com/?id=1"
-
#
-
1
def query=(v)
-
6
return @query = nil unless v
-
4
raise InvalidURIError, "query conflicts with opaque" if @opaque
-
-
4
x = v.to_str
-
4
v = x.dup if x.equal? v
-
4
v.encode!(Encoding::UTF_8) rescue nil
-
4
v.delete!("\t\r\n")
-
4
v.force_encoding(Encoding::ASCII_8BIT)
-
4
raise InvalidURIError, "invalid percent escape: #{$1}" if /(%\H\H)/n.match(v)
-
4
v.gsub!(/(?!%\h\h|[!$-&(-;=?-_a-~])./n.freeze){'%%%02X' % $&.ord}
-
4
v.force_encoding(Encoding::US_ASCII)
-
4
@query = v
-
end
-
-
#
-
# Checks the opaque +v+ component for RFC2396 compliance and
-
# against the URI::Parser Regexp for :OPAQUE.
-
#
-
# Can not have a host, port, user, or path component defined,
-
# with an opaque component defined.
-
#
-
1
def check_opaque(v)
-
return v unless v
-
-
# raise if both hier and opaque are not nil, because:
-
# absoluteURI = scheme ":" ( hier_part | opaque_part )
-
# hier_part = ( net_path | abs_path ) [ "?" query ]
-
if @host || @port || @user || @path # userinfo = @user + ':' + @password
-
raise InvalidURIError,
-
"can not set opaque with host, port, userinfo or path"
-
elsif v && parser.regexp[:OPAQUE] !~ v
-
raise InvalidComponentError,
-
"bad component(expected opaque component): #{v}"
-
end
-
-
return true
-
end
-
1
private :check_opaque
-
-
# Protected setter for the opaque component +v+.
-
#
-
# See also URI::Generic.opaque=.
-
#
-
1
def set_opaque(v)
-
4
@opaque = v
-
end
-
1
protected :set_opaque
-
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the opaque component +v+
-
# (with validation).
-
#
-
# See also URI::Generic.check_opaque.
-
#
-
1
def opaque=(v)
-
check_opaque(v)
-
set_opaque(v)
-
v
-
end
-
-
#
-
# Checks the fragment +v+ component against the URI::Parser Regexp for :FRAGMENT.
-
#
-
#
-
# == Args
-
#
-
# +v+::
-
# String
-
#
-
# == Description
-
#
-
# Public setter for the fragment component +v+
-
# (with validation).
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com/?id=25#time=1305212049")
-
# uri.fragment = "time=1305212086"
-
# uri.to_s #=> "http://my.example.com/?id=25#time=1305212086"
-
#
-
1
def fragment=(v)
-
4
return @fragment = nil unless v
-
-
x = v.to_str
-
v = x.dup if x.equal? v
-
v.encode!(Encoding::UTF_8) rescue nil
-
v.delete!("\t\r\n")
-
v.force_encoding(Encoding::ASCII_8BIT)
-
v.gsub!(/(?!%\h\h|[!-~])./n){'%%%02X' % $&.ord}
-
v.force_encoding(Encoding::US_ASCII)
-
@fragment = v
-
end
-
-
#
-
# Returns true if URI is hierarchical.
-
#
-
# == Description
-
#
-
# URI has components listed in order of decreasing significance from left to right,
-
# see RFC3986 https://tools.ietf.org/html/rfc3986 1.2.3.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com/")
-
# uri.hierarchical?
-
# #=> true
-
# uri = URI.parse("mailto:joe@example.com")
-
# uri.hierarchical?
-
# #=> false
-
#
-
1
def hierarchical?
-
if @path
-
true
-
else
-
false
-
end
-
end
-
-
#
-
# Returns true if URI has a scheme (e.g. http:// or https://) specified.
-
#
-
1
def absolute?
-
if @scheme
-
true
-
else
-
false
-
end
-
end
-
1
alias absolute absolute?
-
-
#
-
# Returns true if URI does not have a scheme (e.g. http:// or https://) specified.
-
#
-
1
def relative?
-
!absolute?
-
end
-
-
#
-
# Returns an Array of the path split on '/'.
-
#
-
1
def split_path(path)
-
path.split("/", -1)
-
end
-
1
private :split_path
-
-
#
-
# Merges a base path +base+, with relative path +rel+,
-
# returns a modified base path.
-
#
-
1
def merge_path(base, rel)
-
-
# RFC2396, Section 5.2, 5)
-
# RFC2396, Section 5.2, 6)
-
base_path = split_path(base)
-
rel_path = split_path(rel)
-
-
# RFC2396, Section 5.2, 6), a)
-
base_path << '' if base_path.last == '..'
-
while i = base_path.index('..')
-
base_path.slice!(i - 1, 2)
-
end
-
-
if (first = rel_path.first) and first.empty?
-
base_path.clear
-
rel_path.shift
-
end
-
-
# RFC2396, Section 5.2, 6), c)
-
# RFC2396, Section 5.2, 6), d)
-
rel_path.push('') if rel_path.last == '.' || rel_path.last == '..'
-
rel_path.delete('.')
-
-
# RFC2396, Section 5.2, 6), e)
-
tmp = []
-
rel_path.each do |x|
-
if x == '..' &&
-
!(tmp.empty? || tmp.last == '..')
-
tmp.pop
-
else
-
tmp << x
-
end
-
end
-
-
add_trailer_slash = !tmp.empty?
-
if base_path.empty?
-
base_path = [''] # keep '/' for root directory
-
elsif add_trailer_slash
-
base_path.pop
-
end
-
while x = tmp.shift
-
if x == '..'
-
# RFC2396, Section 4
-
# a .. or . in an absolute path has no special meaning
-
base_path.pop if base_path.size > 1
-
else
-
# if x == '..'
-
# valid absolute (but abnormal) path "/../..."
-
# else
-
# valid absolute path
-
# end
-
base_path << x
-
tmp.each {|t| base_path << t}
-
add_trailer_slash = false
-
break
-
end
-
end
-
base_path.push('') if add_trailer_slash
-
-
return base_path.join('/')
-
end
-
1
private :merge_path
-
-
#
-
# == Args
-
#
-
# +oth+::
-
# URI or String
-
#
-
# == Description
-
#
-
# Destructive form of #merge.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com")
-
# uri.merge!("/main.rbx?page=1")
-
# uri.to_s # => "http://my.example.com/main.rbx?page=1"
-
#
-
1
def merge!(oth)
-
t = merge(oth)
-
if self == t
-
nil
-
else
-
replace!(t)
-
self
-
end
-
end
-
-
#
-
# == Args
-
#
-
# +oth+::
-
# URI or String
-
#
-
# == Description
-
#
-
# Merges two URIs.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com")
-
# uri.merge("/main.rbx?page=1")
-
# # => "http://my.example.com/main.rbx?page=1"
-
#
-
1
def merge(oth)
-
rel = parser.send(:convert_to_uri, oth)
-
-
if rel.absolute?
-
#raise BadURIError, "both URI are absolute" if absolute?
-
# hmm... should return oth for usability?
-
return rel
-
end
-
-
unless self.absolute?
-
raise BadURIError, "both URI are relative"
-
end
-
-
base = self.dup
-
-
authority = rel.userinfo || rel.host || rel.port
-
-
# RFC2396, Section 5.2, 2)
-
if (rel.path.nil? || rel.path.empty?) && !authority && !rel.query
-
base.fragment=(rel.fragment) if rel.fragment
-
return base
-
end
-
-
base.query = nil
-
base.fragment=(nil)
-
-
# RFC2396, Section 5.2, 4)
-
if !authority
-
base.set_path(merge_path(base.path, rel.path)) if base.path && rel.path
-
else
-
# RFC2396, Section 5.2, 4)
-
base.set_path(rel.path) if rel.path
-
end
-
-
# RFC2396, Section 5.2, 7)
-
base.set_userinfo(rel.userinfo) if rel.userinfo
-
base.set_host(rel.host) if rel.host
-
base.set_port(rel.port) if rel.port
-
base.query = rel.query if rel.query
-
base.fragment=(rel.fragment) if rel.fragment
-
-
return base
-
end # merge
-
1
alias + merge
-
-
# :stopdoc:
-
1
def route_from_path(src, dst)
-
case dst
-
when src
-
# RFC2396, Section 4.2
-
return ''
-
when %r{(?:\A|/)\.\.?(?:/|\z)}
-
# dst has abnormal absolute path,
-
# like "/./", "/../", "/x/../", ...
-
return dst.dup
-
end
-
-
src_path = src.scan(%r{[^/]*/})
-
dst_path = dst.scan(%r{[^/]*/?})
-
-
# discard same parts
-
while !dst_path.empty? && dst_path.first == src_path.first
-
src_path.shift
-
dst_path.shift
-
end
-
-
tmp = dst_path.join
-
-
# calculate
-
if src_path.empty?
-
if tmp.empty?
-
return './'
-
elsif dst_path.first.include?(':') # (see RFC2396 Section 5)
-
return './' + tmp
-
else
-
return tmp
-
end
-
end
-
-
return '../' * src_path.size + tmp
-
end
-
1
private :route_from_path
-
# :startdoc:
-
-
# :stopdoc:
-
1
def route_from0(oth)
-
oth = parser.send(:convert_to_uri, oth)
-
if self.relative?
-
raise BadURIError,
-
"relative URI: #{self}"
-
end
-
if oth.relative?
-
raise BadURIError,
-
"relative URI: #{oth}"
-
end
-
-
if self.scheme != oth.scheme
-
return self, self.dup
-
end
-
rel = URI::Generic.new(nil, # it is relative URI
-
self.userinfo, self.host, self.port,
-
nil, self.path, self.opaque,
-
self.query, self.fragment, parser)
-
-
if rel.userinfo != oth.userinfo ||
-
rel.host.to_s.downcase != oth.host.to_s.downcase ||
-
rel.port != oth.port
-
-
if self.userinfo.nil? && self.host.nil?
-
return self, self.dup
-
end
-
-
rel.set_port(nil) if rel.port == oth.default_port
-
return rel, rel
-
end
-
rel.set_userinfo(nil)
-
rel.set_host(nil)
-
rel.set_port(nil)
-
-
if rel.path && rel.path == oth.path
-
rel.set_path('')
-
rel.query = nil if rel.query == oth.query
-
return rel, rel
-
elsif rel.opaque && rel.opaque == oth.opaque
-
rel.set_opaque('')
-
rel.query = nil if rel.query == oth.query
-
return rel, rel
-
end
-
-
# you can modify `rel', but can not `oth'.
-
return oth, rel
-
end
-
1
private :route_from0
-
# :startdoc:
-
-
#
-
# == Args
-
#
-
# +oth+::
-
# URI or String
-
#
-
# == Description
-
#
-
# Calculates relative path from oth to self.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse('http://my.example.com/main.rbx?page=1')
-
# uri.route_from('http://my.example.com')
-
# #=> #<URI::Generic /main.rbx?page=1>
-
#
-
1
def route_from(oth)
-
# you can modify `rel', but can not `oth'.
-
begin
-
oth, rel = route_from0(oth)
-
rescue
-
raise $!.class, $!.message
-
end
-
if oth == rel
-
return rel
-
end
-
-
rel.set_path(route_from_path(oth.path, self.path))
-
if rel.path == './' && self.query
-
# "./?foo" -> "?foo"
-
rel.set_path('')
-
end
-
-
return rel
-
end
-
-
1
alias - route_from
-
-
#
-
# == Args
-
#
-
# +oth+::
-
# URI or String
-
#
-
# == Description
-
#
-
# Calculates relative path to oth from self.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse('http://my.example.com')
-
# uri.route_to('http://my.example.com/main.rbx?page=1')
-
# #=> #<URI::Generic /main.rbx?page=1>
-
#
-
1
def route_to(oth)
-
parser.send(:convert_to_uri, oth).route_from(self)
-
end
-
-
#
-
# Returns normalized URI.
-
#
-
# require 'uri'
-
#
-
# URI("HTTP://my.EXAMPLE.com").normalize
-
# #=> #<URI::HTTP http://my.example.com/>
-
#
-
# Normalization here means:
-
#
-
# * scheme and host are converted to lowercase,
-
# * an empty path component is set to "/".
-
#
-
1
def normalize
-
uri = dup
-
uri.normalize!
-
uri
-
end
-
-
#
-
# Destructive version of #normalize.
-
#
-
1
def normalize!
-
if path&.empty?
-
set_path('/')
-
end
-
if scheme && scheme != scheme.downcase
-
set_scheme(self.scheme.downcase)
-
end
-
if host && host != host.downcase
-
set_host(self.host.downcase)
-
end
-
end
-
-
#
-
# Constructs String from URI.
-
#
-
1
def to_s
-
2
str = ''.dup
-
2
if @scheme
-
str << @scheme
-
str << ':'
-
end
-
-
2
if @opaque
-
str << @opaque
-
else
-
2
if @host || %w[file postgres].include?(@scheme)
-
2
str << '//'
-
end
-
2
if self.userinfo
-
str << self.userinfo
-
str << '@'
-
end
-
2
if @host
-
2
str << @host
-
end
-
2
if @port && @port != self.default_port
-
str << ':'
-
str << @port.to_s
-
end
-
2
str << @path
-
2
if @query
-
2
str << '?'
-
2
str << @query
-
end
-
end
-
2
if @fragment
-
str << '#'
-
str << @fragment
-
end
-
2
str
-
end
-
-
#
-
# Compares two URIs.
-
#
-
1
def ==(oth)
-
if self.class == oth.class
-
self.normalize.component_ary == oth.normalize.component_ary
-
else
-
false
-
end
-
end
-
-
1
def hash
-
self.component_ary.hash
-
end
-
-
1
def eql?(oth)
-
self.class == oth.class &&
-
parser == oth.parser &&
-
self.component_ary.eql?(oth.component_ary)
-
end
-
-
=begin
-
-
--- URI::Generic#===(oth)
-
-
=end
-
# def ===(oth)
-
# raise NotImplementedError
-
# end
-
-
=begin
-
=end
-
-
-
# Returns an Array of the components defined from the COMPONENT Array.
-
1
def component_ary
-
component.collect do |x|
-
self.send(x)
-
end
-
end
-
1
protected :component_ary
-
-
# == Args
-
#
-
# +components+::
-
# Multiple Symbol arguments defined in URI::HTTP.
-
#
-
# == Description
-
#
-
# Selects specified components from URI.
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse('http://myuser:mypass@my.example.com/test.rbx')
-
# uri.select(:userinfo, :host, :path)
-
# # => ["myuser:mypass", "my.example.com", "/test.rbx"]
-
#
-
1
def select(*components)
-
components.collect do |c|
-
if component.include?(c)
-
self.send(c)
-
else
-
raise ArgumentError,
-
"expected of components of #{self.class} (#{self.class.component.join(', ')})"
-
end
-
end
-
end
-
-
1
def inspect
-
"#<#{self.class} #{self}>"
-
end
-
-
#
-
# == Args
-
#
-
# +v+::
-
# URI or String
-
#
-
# == Description
-
#
-
# Attempts to parse other URI +oth+,
-
# returns [parsed_oth, self].
-
#
-
# == Usage
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("http://my.example.com")
-
# uri.coerce("http://foo.com")
-
# #=> [#<URI::HTTP http://foo.com>, #<URI::HTTP http://my.example.com>]
-
#
-
1
def coerce(oth)
-
case oth
-
when String
-
oth = parser.parse(oth)
-
else
-
super
-
end
-
-
return oth, self
-
end
-
-
# Returns a proxy URI.
-
# The proxy URI is obtained from environment variables such as http_proxy,
-
# ftp_proxy, no_proxy, etc.
-
# If there is no proper proxy, nil is returned.
-
#
-
# If the optional parameter +env+ is specified, it is used instead of ENV.
-
#
-
# Note that capitalized variables (HTTP_PROXY, FTP_PROXY, NO_PROXY, etc.)
-
# are examined, too.
-
#
-
# But http_proxy and HTTP_PROXY is treated specially under CGI environment.
-
# It's because HTTP_PROXY may be set by Proxy: header.
-
# So HTTP_PROXY is not used.
-
# http_proxy is not used too if the variable is case insensitive.
-
# CGI_HTTP_PROXY can be used instead.
-
1
def find_proxy(env=ENV)
-
raise BadURIError, "relative URI: #{self}" if self.relative?
-
name = self.scheme.downcase + '_proxy'
-
proxy_uri = nil
-
if name == 'http_proxy' && env.include?('REQUEST_METHOD') # CGI?
-
# HTTP_PROXY conflicts with *_proxy for proxy settings and
-
# HTTP_* for header information in CGI.
-
# So it should be careful to use it.
-
pairs = env.reject {|k, v| /\Ahttp_proxy\z/i !~ k }
-
case pairs.length
-
when 0 # no proxy setting anyway.
-
proxy_uri = nil
-
when 1
-
k, _ = pairs.shift
-
if k == 'http_proxy' && env[k.upcase] == nil
-
# http_proxy is safe to use because ENV is case sensitive.
-
proxy_uri = env[name]
-
else
-
proxy_uri = nil
-
end
-
else # http_proxy is safe to use because ENV is case sensitive.
-
proxy_uri = env.to_hash[name]
-
end
-
if !proxy_uri
-
# Use CGI_HTTP_PROXY. cf. libwww-perl.
-
proxy_uri = env["CGI_#{name.upcase}"]
-
end
-
elsif name == 'http_proxy'
-
unless proxy_uri = env[name]
-
if proxy_uri = env[name.upcase]
-
warn 'The environment variable HTTP_PROXY is discouraged. Use http_proxy.', uplevel: 1
-
end
-
end
-
else
-
proxy_uri = env[name] || env[name.upcase]
-
end
-
-
if proxy_uri.nil? || proxy_uri.empty?
-
return nil
-
end
-
-
if self.hostname
-
begin
-
addr = IPSocket.getaddress(self.hostname)
-
return nil if /\A127\.|\A::1\z/ =~ addr
-
rescue SocketError
-
end
-
end
-
-
name = 'no_proxy'
-
if no_proxy = env[name] || env[name.upcase]
-
return nil unless URI::Generic.use_proxy?(self.hostname, addr, self.port, no_proxy)
-
end
-
URI.parse(proxy_uri)
-
end
-
-
1
def self.use_proxy?(hostname, addr, port, no_proxy) # :nodoc:
-
hostname = hostname.downcase
-
dothostname = ".#{hostname}"
-
no_proxy.scan(/([^:,\s]+)(?::(\d+))?/) {|p_host, p_port|
-
if !p_port || port == p_port.to_i
-
if p_host.start_with?('.')
-
return false if hostname.end_with?(p_host.downcase)
-
else
-
return false if dothostname.end_with?(".#{p_host.downcase}")
-
end
-
if addr
-
begin
-
return false if IPAddr.new(p_host).include?(addr)
-
rescue IPAddr::InvalidAddressError
-
next
-
end
-
end
-
end
-
}
-
true
-
end
-
end
-
end
-
# frozen_string_literal: false
-
# = uri/http.rb
-
#
-
# Author:: Akira Yamada <akira@ruby-lang.org>
-
# License:: You can redistribute it and/or modify it under the same term as Ruby.
-
# Revision:: $Id$
-
#
-
# See URI for general documentation
-
#
-
-
1
require_relative 'generic'
-
-
1
module URI
-
-
#
-
# The syntax of HTTP URIs is defined in RFC1738 section 3.3.
-
#
-
# Note that the Ruby URI library allows HTTP URLs containing usernames and
-
# passwords. This is not legal as per the RFC, but used to be
-
# supported in Internet Explorer 5 and 6, before the MS04-004 security
-
# update. See <URL:http://support.microsoft.com/kb/834489>.
-
#
-
1
class HTTP < Generic
-
# A Default port of 80 for URI::HTTP.
-
1
DEFAULT_PORT = 80
-
-
# An Array of the available components for URI::HTTP.
-
1
COMPONENT = %i[
-
scheme
-
userinfo host port
-
path
-
query
-
fragment
-
].freeze
-
-
#
-
# == Description
-
#
-
# Creates a new URI::HTTP object from components, with syntax checking.
-
#
-
# The components accepted are userinfo, host, port, path, query, and
-
# fragment.
-
#
-
# The components should be provided either as an Array, or as a Hash
-
# with keys formed by preceding the component names with a colon.
-
#
-
# If an Array is used, the components must be passed in the
-
# order <code>[userinfo, host, port, path, query, fragment]</code>.
-
#
-
# Example:
-
#
-
# uri = URI::HTTP.build(host: 'www.example.com', path: '/foo/bar')
-
#
-
# uri = URI::HTTP.build([nil, "www.example.com", nil, "/path",
-
# "query", 'fragment'])
-
#
-
# Currently, if passed userinfo components this method generates
-
# invalid HTTP URIs as per RFC 1738.
-
#
-
1
def self.build(args)
-
tmp = Util.make_components_hash(self, args)
-
super(tmp)
-
end
-
-
#
-
# == Description
-
#
-
# Returns the full path for an HTTP request, as required by Net::HTTP::Get.
-
#
-
# If the URI contains a query, the full path is URI#path + '?' + URI#query.
-
# Otherwise, the path is simply URI#path.
-
#
-
# Example:
-
#
-
# uri = URI::HTTP.build(path: '/foo/bar', query: 'test=true')
-
# uri.request_uri # => "/foo/bar?test=true"
-
#
-
1
def request_uri
-
return unless @path
-
-
url = @query ? "#@path?#@query" : @path.dup
-
url.start_with?(?/.freeze) ? url : ?/ + url
-
end
-
end
-
-
1
@@schemes['HTTP'] = HTTP
-
-
end
-
# frozen_string_literal: false
-
# = uri/https.rb
-
#
-
# Author:: Akira Yamada <akira@ruby-lang.org>
-
# License:: You can redistribute it and/or modify it under the same term as Ruby.
-
# Revision:: $Id$
-
#
-
# See URI for general documentation
-
#
-
-
1
require_relative 'http'
-
-
1
module URI
-
-
# The default port for HTTPS URIs is 443, and the scheme is 'https:' rather
-
# than 'http:'. Other than that, HTTPS URIs are identical to HTTP URIs;
-
# see URI::HTTP.
-
1
class HTTPS < HTTP
-
# A Default port of 443 for URI::HTTPS
-
1
DEFAULT_PORT = 443
-
end
-
1
@@schemes['HTTPS'] = HTTPS
-
end
-
# frozen_string_literal: false
-
# = uri/ldap.rb
-
#
-
# Author::
-
# Takaaki Tateishi <ttate@jaist.ac.jp>
-
# Akira Yamada <akira@ruby-lang.org>
-
# License::
-
# URI::LDAP is copyrighted free software by Takaaki Tateishi and Akira Yamada.
-
# You can redistribute it and/or modify it under the same term as Ruby.
-
# Revision:: $Id$
-
#
-
# See URI for general documentation
-
#
-
-
1
require_relative 'generic'
-
-
1
module URI
-
-
#
-
# LDAP URI SCHEMA (described in RFC2255).
-
#--
-
# ldap://<host>/<dn>[?<attrs>[?<scope>[?<filter>[?<extensions>]]]]
-
#++
-
1
class LDAP < Generic
-
-
# A Default port of 389 for URI::LDAP.
-
1
DEFAULT_PORT = 389
-
-
# An Array of the available components for URI::LDAP.
-
1
COMPONENT = [
-
:scheme,
-
:host, :port,
-
:dn,
-
:attributes,
-
:scope,
-
:filter,
-
:extensions,
-
].freeze
-
-
# Scopes available for the starting point.
-
#
-
# * SCOPE_BASE - the Base DN
-
# * SCOPE_ONE - one level under the Base DN, not including the base DN and
-
# not including any entries under this
-
# * SCOPE_SUB - subtrees, all entries at all levels
-
#
-
SCOPE = [
-
1
SCOPE_ONE = 'one',
-
SCOPE_SUB = 'sub',
-
SCOPE_BASE = 'base',
-
].freeze
-
-
#
-
# == Description
-
#
-
# Creates a new URI::LDAP object from components, with syntax checking.
-
#
-
# The components accepted are host, port, dn, attributes,
-
# scope, filter, and extensions.
-
#
-
# The components should be provided either as an Array, or as a Hash
-
# with keys formed by preceding the component names with a colon.
-
#
-
# If an Array is used, the components must be passed in the
-
# order <code>[host, port, dn, attributes, scope, filter, extensions]</code>.
-
#
-
# Example:
-
#
-
# uri = URI::LDAP.build({:host => 'ldap.example.com',
-
# :dn => '/dc=example'})
-
#
-
# uri = URI::LDAP.build(["ldap.example.com", nil,
-
# "/dc=example;dc=com", "query", nil, nil, nil])
-
#
-
1
def self.build(args)
-
tmp = Util::make_components_hash(self, args)
-
-
if tmp[:dn]
-
tmp[:path] = tmp[:dn]
-
end
-
-
query = []
-
[:extensions, :filter, :scope, :attributes].collect do |x|
-
next if !tmp[x] && query.size == 0
-
query.unshift(tmp[x])
-
end
-
-
tmp[:query] = query.join('?')
-
-
return super(tmp)
-
end
-
-
#
-
# == Description
-
#
-
# Creates a new URI::LDAP object from generic URI components as per
-
# RFC 2396. No LDAP-specific syntax checking is performed.
-
#
-
# Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+,
-
# +opaque+, +query+, and +fragment+, in that order.
-
#
-
# Example:
-
#
-
# uri = URI::LDAP.new("ldap", nil, "ldap.example.com", nil, nil,
-
# "/dc=example;dc=com", nil, "query", nil)
-
#
-
# See also URI::Generic.new.
-
#
-
1
def initialize(*arg)
-
super(*arg)
-
-
if @fragment
-
raise InvalidURIError, 'bad LDAP URL'
-
end
-
-
parse_dn
-
parse_query
-
end
-
-
# Private method to cleanup +dn+ from using the +path+ component attribute.
-
1
def parse_dn
-
@dn = @path[1..-1]
-
end
-
1
private :parse_dn
-
-
# Private method to cleanup +attributes+, +scope+, +filter+, and +extensions+
-
# from using the +query+ component attribute.
-
1
def parse_query
-
@attributes = nil
-
@scope = nil
-
@filter = nil
-
@extensions = nil
-
-
if @query
-
attrs, scope, filter, extensions = @query.split('?')
-
-
@attributes = attrs if attrs && attrs.size > 0
-
@scope = scope if scope && scope.size > 0
-
@filter = filter if filter && filter.size > 0
-
@extensions = extensions if extensions && extensions.size > 0
-
end
-
end
-
1
private :parse_query
-
-
# Private method to assemble +query+ from +attributes+, +scope+, +filter+, and +extensions+.
-
1
def build_path_query
-
@path = '/' + @dn
-
-
query = []
-
[@extensions, @filter, @scope, @attributes].each do |x|
-
next if !x && query.size == 0
-
query.unshift(x)
-
end
-
@query = query.join('?')
-
end
-
1
private :build_path_query
-
-
# Returns dn.
-
1
def dn
-
@dn
-
end
-
-
# Private setter for dn +val+.
-
1
def set_dn(val)
-
@dn = val
-
build_path_query
-
@dn
-
end
-
1
protected :set_dn
-
-
# Setter for dn +val+.
-
1
def dn=(val)
-
set_dn(val)
-
val
-
end
-
-
# Returns attributes.
-
1
def attributes
-
@attributes
-
end
-
-
# Private setter for attributes +val+.
-
1
def set_attributes(val)
-
@attributes = val
-
build_path_query
-
@attributes
-
end
-
1
protected :set_attributes
-
-
# Setter for attributes +val+.
-
1
def attributes=(val)
-
set_attributes(val)
-
val
-
end
-
-
# Returns scope.
-
1
def scope
-
@scope
-
end
-
-
# Private setter for scope +val+.
-
1
def set_scope(val)
-
@scope = val
-
build_path_query
-
@scope
-
end
-
1
protected :set_scope
-
-
# Setter for scope +val+.
-
1
def scope=(val)
-
set_scope(val)
-
val
-
end
-
-
# Returns filter.
-
1
def filter
-
@filter
-
end
-
-
# Private setter for filter +val+.
-
1
def set_filter(val)
-
@filter = val
-
build_path_query
-
@filter
-
end
-
1
protected :set_filter
-
-
# Setter for filter +val+.
-
1
def filter=(val)
-
set_filter(val)
-
val
-
end
-
-
# Returns extensions.
-
1
def extensions
-
@extensions
-
end
-
-
# Private setter for extensions +val+.
-
1
def set_extensions(val)
-
@extensions = val
-
build_path_query
-
@extensions
-
end
-
1
protected :set_extensions
-
-
# Setter for extensions +val+.
-
1
def extensions=(val)
-
set_extensions(val)
-
val
-
end
-
-
# Checks if URI has a path.
-
# For URI::LDAP this will return +false+.
-
1
def hierarchical?
-
false
-
end
-
end
-
-
1
@@schemes['LDAP'] = LDAP
-
end
-
# frozen_string_literal: false
-
# = uri/ldap.rb
-
#
-
# License:: You can redistribute it and/or modify it under the same term as Ruby.
-
#
-
# See URI for general documentation
-
#
-
-
1
require_relative 'ldap'
-
-
1
module URI
-
-
# The default port for LDAPS URIs is 636, and the scheme is 'ldaps:' rather
-
# than 'ldap:'. Other than that, LDAPS URIs are identical to LDAP URIs;
-
# see URI::LDAP.
-
1
class LDAPS < LDAP
-
# A Default port of 636 for URI::LDAPS
-
1
DEFAULT_PORT = 636
-
end
-
1
@@schemes['LDAPS'] = LDAPS
-
end
-
# frozen_string_literal: false
-
# = uri/mailto.rb
-
#
-
# Author:: Akira Yamada <akira@ruby-lang.org>
-
# License:: You can redistribute it and/or modify it under the same term as Ruby.
-
# Revision:: $Id$
-
#
-
# See URI for general documentation
-
#
-
-
1
require_relative 'generic'
-
-
1
module URI
-
-
#
-
# RFC6068, the mailto URL scheme.
-
#
-
1
class MailTo < Generic
-
1
include REGEXP
-
-
# A Default port of nil for URI::MailTo.
-
1
DEFAULT_PORT = nil
-
-
# An Array of the available components for URI::MailTo.
-
1
COMPONENT = [ :scheme, :to, :headers ].freeze
-
-
# :stopdoc:
-
# "hname" and "hvalue" are encodings of an RFC 822 header name and
-
# value, respectively. As with "to", all URL reserved characters must
-
# be encoded.
-
#
-
# "#mailbox" is as specified in RFC 822 [RFC822]. This means that it
-
# consists of zero or more comma-separated mail addresses, possibly
-
# including "phrase" and "comment" components. Note that all URL
-
# reserved characters in "to" must be encoded: in particular,
-
# parentheses, commas, and the percent sign ("%"), which commonly occur
-
# in the "mailbox" syntax.
-
#
-
# Within mailto URLs, the characters "?", "=", "&" are reserved.
-
-
# ; RFC 6068
-
# hfields = "?" hfield *( "&" hfield )
-
# hfield = hfname "=" hfvalue
-
# hfname = *qchar
-
# hfvalue = *qchar
-
# qchar = unreserved / pct-encoded / some-delims
-
# some-delims = "!" / "$" / "'" / "(" / ")" / "*"
-
# / "+" / "," / ";" / ":" / "@"
-
#
-
# ; RFC3986
-
# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
-
# pct-encoded = "%" HEXDIG HEXDIG
-
1
HEADER_REGEXP = /\A(?<hfield>(?:%\h\h|[!$'-.0-;@-Z_a-z~])*=(?:%\h\h|[!$'-.0-;@-Z_a-z~])*)(?:&\g<hfield>)*\z/
-
# practical regexp for email address
-
# https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
-
1
EMAIL_REGEXP = /\A[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\z/
-
# :startdoc:
-
-
#
-
# == Description
-
#
-
# Creates a new URI::MailTo object from components, with syntax checking.
-
#
-
# Components can be provided as an Array or Hash. If an Array is used,
-
# the components must be supplied as <code>[to, headers]</code>.
-
#
-
# If a Hash is used, the keys are the component names preceded by colons.
-
#
-
# The headers can be supplied as a pre-encoded string, such as
-
# <code>"subject=subscribe&cc=address"</code>, or as an Array of Arrays
-
# like <code>[['subject', 'subscribe'], ['cc', 'address']]</code>.
-
#
-
# Examples:
-
#
-
# require 'uri'
-
#
-
# m1 = URI::MailTo.build(['joe@example.com', 'subject=Ruby'])
-
# m1.to_s # => "mailto:joe@example.com?subject=Ruby"
-
#
-
# m2 = URI::MailTo.build(['john@example.com', [['Subject', 'Ruby'], ['Cc', 'jack@example.com']]])
-
# m2.to_s # => "mailto:john@example.com?Subject=Ruby&Cc=jack@example.com"
-
#
-
# m3 = URI::MailTo.build({:to => 'listman@example.com', :headers => [['subject', 'subscribe']]})
-
# m3.to_s # => "mailto:listman@example.com?subject=subscribe"
-
#
-
1
def self.build(args)
-
tmp = Util.make_components_hash(self, args)
-
-
case tmp[:to]
-
when Array
-
tmp[:opaque] = tmp[:to].join(',')
-
when String
-
tmp[:opaque] = tmp[:to].dup
-
else
-
tmp[:opaque] = ''
-
end
-
-
if tmp[:headers]
-
query =
-
case tmp[:headers]
-
when Array
-
tmp[:headers].collect { |x|
-
if x.kind_of?(Array)
-
x[0] + '=' + x[1..-1].join
-
else
-
x.to_s
-
end
-
}.join('&')
-
when Hash
-
tmp[:headers].collect { |h,v|
-
h + '=' + v
-
}.join('&')
-
else
-
tmp[:headers].to_s
-
end
-
unless query.empty?
-
tmp[:opaque] << '?' << query
-
end
-
end
-
-
super(tmp)
-
end
-
-
#
-
# == Description
-
#
-
# Creates a new URI::MailTo object from generic URL components with
-
# no syntax checking.
-
#
-
# This method is usually called from URI::parse, which checks
-
# the validity of each component.
-
#
-
1
def initialize(*arg)
-
super(*arg)
-
-
@to = nil
-
@headers = []
-
-
# The RFC3986 parser does not normally populate opaque
-
@opaque = "?#{@query}" if @query && !@opaque
-
-
unless @opaque
-
raise InvalidComponentError,
-
"missing opaque part for mailto URL"
-
end
-
to, header = @opaque.split('?', 2)
-
# allow semicolon as a addr-spec separator
-
# http://support.microsoft.com/kb/820868
-
unless /\A(?:[^@,;]+@[^@,;]+(?:\z|[,;]))*\z/ =~ to
-
raise InvalidComponentError,
-
"unrecognised opaque part for mailtoURL: #{@opaque}"
-
end
-
-
if arg[10] # arg_check
-
self.to = to
-
self.headers = header
-
else
-
set_to(to)
-
set_headers(header)
-
end
-
end
-
-
# The primary e-mail address of the URL, as a String.
-
1
attr_reader :to
-
-
# E-mail headers set by the URL, as an Array of Arrays.
-
1
attr_reader :headers
-
-
# Checks the to +v+ component.
-
1
def check_to(v)
-
return true unless v
-
return true if v.size == 0
-
-
v.split(/[,;]/).each do |addr|
-
# check url safety as path-rootless
-
if /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*\z/ !~ addr
-
raise InvalidComponentError,
-
"an address in 'to' is invalid as URI #{addr.dump}"
-
end
-
-
# check addr-spec
-
# don't s/\+/ /g
-
addr.gsub!(/%\h\h/, URI::TBLDECWWWCOMP_)
-
if EMAIL_REGEXP !~ addr
-
raise InvalidComponentError,
-
"an address in 'to' is invalid as uri-escaped addr-spec #{addr.dump}"
-
end
-
end
-
-
true
-
end
-
1
private :check_to
-
-
# Private setter for to +v+.
-
1
def set_to(v)
-
@to = v
-
end
-
1
protected :set_to
-
-
# Setter for to +v+.
-
1
def to=(v)
-
check_to(v)
-
set_to(v)
-
v
-
end
-
-
# Checks the headers +v+ component against either
-
# * HEADER_REGEXP
-
1
def check_headers(v)
-
return true unless v
-
return true if v.size == 0
-
if HEADER_REGEXP !~ v
-
raise InvalidComponentError,
-
"bad component(expected opaque component): #{v}"
-
end
-
-
true
-
end
-
1
private :check_headers
-
-
# Private setter for headers +v+.
-
1
def set_headers(v)
-
@headers = []
-
if v
-
v.split('&').each do |x|
-
@headers << x.split(/=/, 2)
-
end
-
end
-
end
-
1
protected :set_headers
-
-
# Setter for headers +v+.
-
1
def headers=(v)
-
check_headers(v)
-
set_headers(v)
-
v
-
end
-
-
# Constructs String from URI.
-
1
def to_s
-
@scheme + ':' +
-
if @to
-
@to
-
else
-
''
-
end +
-
if @headers.size > 0
-
'?' + @headers.collect{|x| x.join('=')}.join('&')
-
else
-
''
-
end +
-
if @fragment
-
'#' + @fragment
-
else
-
''
-
end
-
end
-
-
# Returns the RFC822 e-mail text equivalent of the URL, as a String.
-
#
-
# Example:
-
#
-
# require 'uri'
-
#
-
# uri = URI.parse("mailto:ruby-list@ruby-lang.org?Subject=subscribe&cc=myaddr")
-
# uri.to_mailtext
-
# # => "To: ruby-list@ruby-lang.org\nSubject: subscribe\nCc: myaddr\n\n\n"
-
#
-
1
def to_mailtext
-
to = URI.decode_www_form_component(@to)
-
head = ''
-
body = ''
-
@headers.each do |x|
-
case x[0]
-
when 'body'
-
body = URI.decode_www_form_component(x[1])
-
when 'to'
-
to << ', ' + URI.decode_www_form_component(x[1])
-
else
-
head << URI.decode_www_form_component(x[0]).capitalize + ': ' +
-
URI.decode_www_form_component(x[1]) + "\n"
-
end
-
end
-
-
"To: #{to}
-
#{head}
-
#{body}
-
"
-
end
-
1
alias to_rfc822text to_mailtext
-
end
-
-
1
@@schemes['MAILTO'] = MailTo
-
end
-
# frozen_string_literal: false
-
#--
-
# = uri/common.rb
-
#
-
# Author:: Akira Yamada <akira@ruby-lang.org>
-
# Revision:: $Id$
-
# License::
-
# You can redistribute it and/or modify it under the same term as Ruby.
-
#
-
# See URI for general documentation
-
#
-
-
1
module URI
-
#
-
# Includes URI::REGEXP::PATTERN
-
#
-
1
module RFC2396_REGEXP
-
#
-
# Patterns used to parse URI's
-
#
-
1
module PATTERN
-
# :stopdoc:
-
-
# RFC 2396 (URI Generic Syntax)
-
# RFC 2732 (IPv6 Literal Addresses in URL's)
-
# RFC 2373 (IPv6 Addressing Architecture)
-
-
# alpha = lowalpha | upalpha
-
1
ALPHA = "a-zA-Z"
-
# alphanum = alpha | digit
-
1
ALNUM = "#{ALPHA}\\d"
-
-
# hex = digit | "A" | "B" | "C" | "D" | "E" | "F" |
-
# "a" | "b" | "c" | "d" | "e" | "f"
-
1
HEX = "a-fA-F\\d"
-
# escaped = "%" hex hex
-
1
ESCAPED = "%[#{HEX}]{2}"
-
# mark = "-" | "_" | "." | "!" | "~" | "*" | "'" |
-
# "(" | ")"
-
# unreserved = alphanum | mark
-
1
UNRESERVED = "\\-_.!~*'()#{ALNUM}"
-
# reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
-
# "$" | ","
-
# reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
-
# "$" | "," | "[" | "]" (RFC 2732)
-
1
RESERVED = ";/?:@&=+$,\\[\\]"
-
-
# domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
-
1
DOMLABEL = "(?:[#{ALNUM}](?:[-#{ALNUM}]*[#{ALNUM}])?)"
-
# toplabel = alpha | alpha *( alphanum | "-" ) alphanum
-
1
TOPLABEL = "(?:[#{ALPHA}](?:[-#{ALNUM}]*[#{ALNUM}])?)"
-
# hostname = *( domainlabel "." ) toplabel [ "." ]
-
1
HOSTNAME = "(?:#{DOMLABEL}\\.)*#{TOPLABEL}\\.?"
-
-
# :startdoc:
-
end # PATTERN
-
-
# :startdoc:
-
end # REGEXP
-
-
# Class that parses String's into URI's.
-
#
-
# It contains a Hash set of patterns and Regexp's that match and validate.
-
#
-
1
class RFC2396_Parser
-
1
include RFC2396_REGEXP
-
-
#
-
# == Synopsis
-
#
-
# URI::Parser.new([opts])
-
#
-
# == Args
-
#
-
# The constructor accepts a hash as options for parser.
-
# Keys of options are pattern names of URI components
-
# and values of options are pattern strings.
-
# The constructor generates set of regexps for parsing URIs.
-
#
-
# You can use the following keys:
-
#
-
# * :ESCAPED (URI::PATTERN::ESCAPED in default)
-
# * :UNRESERVED (URI::PATTERN::UNRESERVED in default)
-
# * :DOMLABEL (URI::PATTERN::DOMLABEL in default)
-
# * :TOPLABEL (URI::PATTERN::TOPLABEL in default)
-
# * :HOSTNAME (URI::PATTERN::HOSTNAME in default)
-
#
-
# == Examples
-
#
-
# p = URI::Parser.new(:ESCAPED => "(?:%[a-fA-F0-9]{2}|%u[a-fA-F0-9]{4})")
-
# u = p.parse("http://example.jp/%uABCD") #=> #<URI::HTTP http://example.jp/%uABCD>
-
# URI.parse(u.to_s) #=> raises URI::InvalidURIError
-
#
-
# s = "http://example.com/ABCD"
-
# u1 = p.parse(s) #=> #<URI::HTTP http://example.com/ABCD>
-
# u2 = URI.parse(s) #=> #<URI::HTTP http://example.com/ABCD>
-
# u1 == u2 #=> true
-
# u1.eql?(u2) #=> false
-
#
-
1
def initialize(opts = {})
-
4
@pattern = initialize_pattern(opts)
-
4
@pattern.each_value(&:freeze)
-
4
@pattern.freeze
-
-
4
@regexp = initialize_regexp(@pattern)
-
4
@regexp.each_value(&:freeze)
-
4
@regexp.freeze
-
end
-
-
# The Hash of patterns.
-
#
-
# See also URI::Parser.initialize_pattern.
-
1
attr_reader :pattern
-
-
# The Hash of Regexp.
-
#
-
# See also URI::Parser.initialize_regexp.
-
1
attr_reader :regexp
-
-
# Returns a split URI against regexp[:ABS_URI].
-
1
def split(uri)
-
2
case uri
-
when ''
-
# null uri
-
-
when @regexp[:ABS_URI]
-
scheme, opaque, userinfo, host, port,
-
registry, path, query, fragment = $~[1..-1]
-
-
# URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
-
-
# absoluteURI = scheme ":" ( hier_part | opaque_part )
-
# hier_part = ( net_path | abs_path ) [ "?" query ]
-
# opaque_part = uric_no_slash *uric
-
-
# abs_path = "/" path_segments
-
# net_path = "//" authority [ abs_path ]
-
-
# authority = server | reg_name
-
# server = [ [ userinfo "@" ] hostport ]
-
-
if !scheme
-
raise InvalidURIError,
-
"bad URI(absolute but no scheme): #{uri}"
-
end
-
if !opaque && (!path && (!host && !registry))
-
raise InvalidURIError,
-
"bad URI(absolute but no path): #{uri}"
-
end
-
-
when @regexp[:REL_URI]
-
2
scheme = nil
-
2
opaque = nil
-
-
userinfo, host, port, registry,
-
2
rel_segment, abs_path, query, fragment = $~[1..-1]
-
2
if rel_segment && abs_path
-
path = rel_segment + abs_path
-
2
elsif rel_segment
-
path = rel_segment
-
2
elsif abs_path
-
2
path = abs_path
-
end
-
-
# URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
-
-
# relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ]
-
-
# net_path = "//" authority [ abs_path ]
-
# abs_path = "/" path_segments
-
# rel_path = rel_segment [ abs_path ]
-
-
# authority = server | reg_name
-
# server = [ [ userinfo "@" ] hostport ]
-
-
else
-
raise InvalidURIError, "bad URI(is not URI?): #{uri}"
-
end
-
-
2
path = '' if !path && !opaque # (see RFC2396 Section 5.2)
-
ret = [
-
2
scheme,
-
userinfo, host, port, # X
-
registry, # X
-
path, # Y
-
opaque, # Y
-
query,
-
fragment
-
]
-
2
return ret
-
end
-
-
#
-
# == Args
-
#
-
# +uri+::
-
# String
-
#
-
# == Description
-
#
-
# Parses +uri+ and constructs either matching URI scheme object
-
# (File, FTP, HTTP, HTTPS, LDAP, LDAPS, or MailTo) or URI::Generic.
-
#
-
# == Usage
-
#
-
# p = URI::Parser.new
-
# p.parse("ldap://ldap.example.com/dc=example?user=john")
-
# #=> #<URI::LDAP ldap://ldap.example.com/dc=example?user=john>
-
#
-
1
def parse(uri)
-
scheme, userinfo, host, port,
-
2
registry, path, opaque, query, fragment = self.split(uri)
-
-
2
if scheme && URI.scheme_list.include?(scheme.upcase)
-
URI.scheme_list[scheme.upcase].new(scheme, userinfo, host, port,
-
registry, path, opaque, query,
-
fragment, self)
-
else
-
2
Generic.new(scheme, userinfo, host, port,
-
registry, path, opaque, query,
-
fragment, self)
-
end
-
end
-
-
-
#
-
# == Args
-
#
-
# +uris+::
-
# an Array of Strings
-
#
-
# == Description
-
#
-
# Attempts to parse and merge a set of URIs.
-
#
-
1
def join(*uris)
-
uris[0] = convert_to_uri(uris[0])
-
uris.inject :merge
-
end
-
-
#
-
# :call-seq:
-
# extract( str )
-
# extract( str, schemes )
-
# extract( str, schemes ) {|item| block }
-
#
-
# == Args
-
#
-
# +str+::
-
# String to search
-
# +schemes+::
-
# Patterns to apply to +str+
-
#
-
# == Description
-
#
-
# Attempts to parse and merge a set of URIs.
-
# If no +block+ given, then returns the result,
-
# else it calls +block+ for each element in result.
-
#
-
# See also URI::Parser.make_regexp.
-
#
-
1
def extract(str, schemes = nil)
-
if block_given?
-
str.scan(make_regexp(schemes)) { yield $& }
-
nil
-
else
-
result = []
-
str.scan(make_regexp(schemes)) { result.push $& }
-
result
-
end
-
end
-
-
# Returns Regexp that is default self.regexp[:ABS_URI_REF],
-
# unless +schemes+ is provided. Then it is a Regexp.union with self.pattern[:X_ABS_URI].
-
1
def make_regexp(schemes = nil)
-
unless schemes
-
@regexp[:ABS_URI_REF]
-
else
-
/(?=#{Regexp.union(*schemes)}:)#{@pattern[:X_ABS_URI]}/x
-
end
-
end
-
-
#
-
# :call-seq:
-
# escape( str )
-
# escape( str, unsafe )
-
#
-
# == Args
-
#
-
# +str+::
-
# String to make safe
-
# +unsafe+::
-
# Regexp to apply. Defaults to self.regexp[:UNSAFE]
-
#
-
# == Description
-
#
-
# Constructs a safe String from +str+, removing unsafe characters,
-
# replacing them with codes.
-
#
-
1
def escape(str, unsafe = @regexp[:UNSAFE])
-
139
unless unsafe.kind_of?(Regexp)
-
# perhaps unsafe is String object
-
unsafe = Regexp.new("[#{Regexp.quote(unsafe)}]", false)
-
end
-
139
str.gsub(unsafe) do
-
58
us = $&
-
58
tmp = ''
-
58
us.each_byte do |uc|
-
58
tmp << sprintf('%%%02X', uc)
-
end
-
58
tmp
-
end.force_encoding(Encoding::US_ASCII)
-
end
-
-
#
-
# :call-seq:
-
# unescape( str )
-
# unescape( str, escaped )
-
#
-
# == Args
-
#
-
# +str+::
-
# String to remove escapes from
-
# +escaped+::
-
# Regexp to apply. Defaults to self.regexp[:ESCAPED]
-
#
-
# == Description
-
#
-
# Removes escapes from +str+.
-
#
-
1
def unescape(str, escaped = @regexp[:ESCAPED])
-
57
enc = str.encoding
-
57
enc = Encoding::UTF_8 if enc == Encoding::US_ASCII
-
57
str.gsub(escaped) { [$&[1, 2]].pack('H2').force_encoding(enc) }
-
end
-
-
1
@@to_s = Kernel.instance_method(:to_s)
-
1
def inspect
-
@@to_s.bind_call(self)
-
end
-
-
1
private
-
-
# Constructs the default Hash of patterns.
-
1
def initialize_pattern(opts = {})
-
4
ret = {}
-
4
ret[:ESCAPED] = escaped = (opts.delete(:ESCAPED) || PATTERN::ESCAPED)
-
4
ret[:UNRESERVED] = unreserved = opts.delete(:UNRESERVED) || PATTERN::UNRESERVED
-
4
ret[:RESERVED] = reserved = opts.delete(:RESERVED) || PATTERN::RESERVED
-
4
ret[:DOMLABEL] = opts.delete(:DOMLABEL) || PATTERN::DOMLABEL
-
4
ret[:TOPLABEL] = opts.delete(:TOPLABEL) || PATTERN::TOPLABEL
-
4
ret[:HOSTNAME] = hostname = opts.delete(:HOSTNAME)
-
-
# RFC 2396 (URI Generic Syntax)
-
# RFC 2732 (IPv6 Literal Addresses in URL's)
-
# RFC 2373 (IPv6 Addressing Architecture)
-
-
# uric = reserved | unreserved | escaped
-
4
ret[:URIC] = uric = "(?:[#{unreserved}#{reserved}]|#{escaped})"
-
# uric_no_slash = unreserved | escaped | ";" | "?" | ":" | "@" |
-
# "&" | "=" | "+" | "$" | ","
-
4
ret[:URIC_NO_SLASH] = uric_no_slash = "(?:[#{unreserved};?:@&=+$,]|#{escaped})"
-
# query = *uric
-
4
ret[:QUERY] = query = "#{uric}*"
-
# fragment = *uric
-
4
ret[:FRAGMENT] = fragment = "#{uric}*"
-
-
# hostname = *( domainlabel "." ) toplabel [ "." ]
-
# reg-name = *( unreserved / pct-encoded / sub-delims ) # RFC3986
-
4
unless hostname
-
4
ret[:HOSTNAME] = hostname = "(?:[a-zA-Z0-9\\-.]|%\\h\\h)+"
-
end
-
-
# RFC 2373, APPENDIX B:
-
# IPv6address = hexpart [ ":" IPv4address ]
-
# IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
-
# hexpart = hexseq | hexseq "::" [ hexseq ] | "::" [ hexseq ]
-
# hexseq = hex4 *( ":" hex4)
-
# hex4 = 1*4HEXDIG
-
#
-
# XXX: This definition has a flaw. "::" + IPv4address must be
-
# allowed too. Here is a replacement.
-
#
-
# IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
-
4
ret[:IPV4ADDR] = ipv4addr = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"
-
# hex4 = 1*4HEXDIG
-
4
hex4 = "[#{PATTERN::HEX}]{1,4}"
-
# lastpart = hex4 | IPv4address
-
4
lastpart = "(?:#{hex4}|#{ipv4addr})"
-
# hexseq1 = *( hex4 ":" ) hex4
-
4
hexseq1 = "(?:#{hex4}:)*#{hex4}"
-
# hexseq2 = *( hex4 ":" ) lastpart
-
4
hexseq2 = "(?:#{hex4}:)*#{lastpart}"
-
# IPv6address = hexseq2 | [ hexseq1 ] "::" [ hexseq2 ]
-
4
ret[:IPV6ADDR] = ipv6addr = "(?:#{hexseq2}|(?:#{hexseq1})?::(?:#{hexseq2})?)"
-
-
# IPv6prefix = ( hexseq1 | [ hexseq1 ] "::" [ hexseq1 ] ) "/" 1*2DIGIT
-
# unused
-
-
# ipv6reference = "[" IPv6address "]" (RFC 2732)
-
4
ret[:IPV6REF] = ipv6ref = "\\[#{ipv6addr}\\]"
-
-
# host = hostname | IPv4address
-
# host = hostname | IPv4address | IPv6reference (RFC 2732)
-
4
ret[:HOST] = host = "(?:#{hostname}|#{ipv4addr}|#{ipv6ref})"
-
# port = *digit
-
4
ret[:PORT] = port = '\d*'
-
# hostport = host [ ":" port ]
-
4
ret[:HOSTPORT] = hostport = "#{host}(?::#{port})?"
-
-
# userinfo = *( unreserved | escaped |
-
# ";" | ":" | "&" | "=" | "+" | "$" | "," )
-
4
ret[:USERINFO] = userinfo = "(?:[#{unreserved};:&=+$,]|#{escaped})*"
-
-
# pchar = unreserved | escaped |
-
# ":" | "@" | "&" | "=" | "+" | "$" | ","
-
4
pchar = "(?:[#{unreserved}:@&=+$,]|#{escaped})"
-
# param = *pchar
-
4
param = "#{pchar}*"
-
# segment = *pchar *( ";" param )
-
4
segment = "#{pchar}*(?:;#{param})*"
-
# path_segments = segment *( "/" segment )
-
4
ret[:PATH_SEGMENTS] = path_segments = "#{segment}(?:/#{segment})*"
-
-
# server = [ [ userinfo "@" ] hostport ]
-
4
server = "(?:#{userinfo}@)?#{hostport}"
-
# reg_name = 1*( unreserved | escaped | "$" | "," |
-
# ";" | ":" | "@" | "&" | "=" | "+" )
-
4
ret[:REG_NAME] = reg_name = "(?:[#{unreserved}$,;:@&=+]|#{escaped})+"
-
# authority = server | reg_name
-
4
authority = "(?:#{server}|#{reg_name})"
-
-
# rel_segment = 1*( unreserved | escaped |
-
# ";" | "@" | "&" | "=" | "+" | "$" | "," )
-
4
ret[:REL_SEGMENT] = rel_segment = "(?:[#{unreserved};@&=+$,]|#{escaped})+"
-
-
# scheme = alpha *( alpha | digit | "+" | "-" | "." )
-
4
ret[:SCHEME] = scheme = "[#{PATTERN::ALPHA}][\\-+.#{PATTERN::ALPHA}\\d]*"
-
-
# abs_path = "/" path_segments
-
4
ret[:ABS_PATH] = abs_path = "/#{path_segments}"
-
# rel_path = rel_segment [ abs_path ]
-
4
ret[:REL_PATH] = rel_path = "#{rel_segment}(?:#{abs_path})?"
-
# net_path = "//" authority [ abs_path ]
-
4
ret[:NET_PATH] = net_path = "//#{authority}(?:#{abs_path})?"
-
-
# hier_part = ( net_path | abs_path ) [ "?" query ]
-
4
ret[:HIER_PART] = hier_part = "(?:#{net_path}|#{abs_path})(?:\\?(?:#{query}))?"
-
# opaque_part = uric_no_slash *uric
-
4
ret[:OPAQUE_PART] = opaque_part = "#{uric_no_slash}#{uric}*"
-
-
# absoluteURI = scheme ":" ( hier_part | opaque_part )
-
4
ret[:ABS_URI] = abs_uri = "#{scheme}:(?:#{hier_part}|#{opaque_part})"
-
# relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ]
-
4
ret[:REL_URI] = rel_uri = "(?:#{net_path}|#{abs_path}|#{rel_path})(?:\\?#{query})?"
-
-
# URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
-
4
ret[:URI_REF] = "(?:#{abs_uri}|#{rel_uri})?(?:##{fragment})?"
-
-
4
ret[:X_ABS_URI] = "
-
(#{scheme}): (?# 1: scheme)
-
(?:
-
(#{opaque_part}) (?# 2: opaque)
-
|
-
(?:(?:
-
//(?:
-
(?:(?:(#{userinfo})@)? (?# 3: userinfo)
-
(?:(#{host})(?::(\\d*))?))? (?# 4: host, 5: port)
-
|
-
(#{reg_name}) (?# 6: registry)
-
)
-
|
-
(?!//)) (?# XXX: '//' is the mark for hostport)
-
(#{abs_path})? (?# 7: path)
-
)(?:\\?(#{query}))? (?# 8: query)
-
)
-
(?:\\#(#{fragment}))? (?# 9: fragment)
-
"
-
-
4
ret[:X_REL_URI] = "
-
(?:
-
(?:
-
//
-
(?:
-
(?:(#{userinfo})@)? (?# 1: userinfo)
-
(#{host})?(?::(\\d*))? (?# 2: host, 3: port)
-
|
-
(#{reg_name}) (?# 4: registry)
-
)
-
)
-
|
-
(#{rel_segment}) (?# 5: rel_segment)
-
)?
-
(#{abs_path})? (?# 6: abs_path)
-
(?:\\?(#{query}))? (?# 7: query)
-
(?:\\#(#{fragment}))? (?# 8: fragment)
-
"
-
-
4
ret
-
end
-
-
# Constructs the default Hash of Regexp's.
-
1
def initialize_regexp(pattern)
-
4
ret = {}
-
-
# for URI::split
-
4
ret[:ABS_URI] = Regexp.new('\A\s*' + pattern[:X_ABS_URI] + '\s*\z', Regexp::EXTENDED)
-
4
ret[:REL_URI] = Regexp.new('\A\s*' + pattern[:X_REL_URI] + '\s*\z', Regexp::EXTENDED)
-
-
# for URI::extract
-
4
ret[:URI_REF] = Regexp.new(pattern[:URI_REF])
-
4
ret[:ABS_URI_REF] = Regexp.new(pattern[:X_ABS_URI], Regexp::EXTENDED)
-
4
ret[:REL_URI_REF] = Regexp.new(pattern[:X_REL_URI], Regexp::EXTENDED)
-
-
# for URI::escape/unescape
-
4
ret[:ESCAPED] = Regexp.new(pattern[:ESCAPED])
-
4
ret[:UNSAFE] = Regexp.new("[^#{pattern[:UNRESERVED]}#{pattern[:RESERVED]}]")
-
-
# for Generic#initialize
-
4
ret[:SCHEME] = Regexp.new("\\A#{pattern[:SCHEME]}\\z")
-
4
ret[:USERINFO] = Regexp.new("\\A#{pattern[:USERINFO]}\\z")
-
4
ret[:HOST] = Regexp.new("\\A#{pattern[:HOST]}\\z")
-
4
ret[:PORT] = Regexp.new("\\A#{pattern[:PORT]}\\z")
-
4
ret[:OPAQUE] = Regexp.new("\\A#{pattern[:OPAQUE_PART]}\\z")
-
4
ret[:REGISTRY] = Regexp.new("\\A#{pattern[:REG_NAME]}\\z")
-
4
ret[:ABS_PATH] = Regexp.new("\\A#{pattern[:ABS_PATH]}\\z")
-
4
ret[:REL_PATH] = Regexp.new("\\A#{pattern[:REL_PATH]}\\z")
-
4
ret[:QUERY] = Regexp.new("\\A#{pattern[:QUERY]}\\z")
-
4
ret[:FRAGMENT] = Regexp.new("\\A#{pattern[:FRAGMENT]}\\z")
-
-
4
ret
-
end
-
-
1
def convert_to_uri(uri)
-
if uri.is_a?(URI::Generic)
-
uri
-
elsif uri = String.try_convert(uri)
-
parse(uri)
-
else
-
raise ArgumentError,
-
"bad argument (expected URI object or URI string)"
-
end
-
end
-
-
end # class Parser
-
end # module URI
-
# frozen_string_literal: false
-
1
module URI
-
1
class RFC3986_Parser # :nodoc:
-
# URI defined in RFC3986
-
# this regexp is modified not to host is not empty string
-
1
RFC3986_URI = /\A(?<URI>(?<scheme>[A-Za-z][+\-.0-9A-Za-z]*):(?<hier-part>\/\/(?<authority>(?:(?<userinfo>(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*)@)?(?<host>(?<IP-literal>\[(?:(?<IPv6address>(?:\h{1,4}:){6}(?<ls32>\h{1,4}:\h{1,4}|(?<IPv4address>(?<dec-octet>[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g<dec-octet>\.\g<dec-octet>\.\g<dec-octet>))|::(?:\h{1,4}:){5}\g<ls32>|\h{1,4}?::(?:\h{1,4}:){4}\g<ls32>|(?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g<ls32>|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g<ls32>|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g<ls32>|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g<ls32>|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?<IPvFuture>v\h+\.[!$&-.0-;=A-Z_a-z~]+))\])|\g<IPv4address>|(?<reg-name>(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])+))?(?::(?<port>\d*))?)(?<path-abempty>(?:\/(?<segment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*))*)|(?<path-absolute>\/(?:(?<segment-nz>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+)(?:\/\g<segment>)*)?)|(?<path-rootless>\g<segment-nz>(?:\/\g<segment>)*)|(?<path-empty>))(?:\?(?<query>[^#]*))?(?:\#(?<fragment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*))?)\z/
-
1
RFC3986_relative_ref = /\A(?<relative-ref>(?<relative-part>\/\/(?<authority>(?:(?<userinfo>(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*)@)?(?<host>(?<IP-literal>\[(?<IPv6address>(?:\h{1,4}:){6}(?<ls32>\h{1,4}:\h{1,4}|(?<IPv4address>(?<dec-octet>[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g<dec-octet>\.\g<dec-octet>\.\g<dec-octet>))|::(?:\h{1,4}:){5}\g<ls32>|\h{1,4}?::(?:\h{1,4}:){4}\g<ls32>|(?:(?:\h{1,4}:){,1}\h{1,4})?::(?:\h{1,4}:){3}\g<ls32>|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g<ls32>|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g<ls32>|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g<ls32>|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?<IPvFuture>v\h+\.[!$&-.0-;=A-Z_a-z~]+)\])|\g<IPv4address>|(?<reg-name>(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])+))?(?::(?<port>\d*))?)(?<path-abempty>(?:\/(?<segment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*))*)|(?<path-absolute>\/(?:(?<segment-nz>(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+)(?:\/\g<segment>)*)?)|(?<path-noscheme>(?<segment-nz-nc>(?:%\h\h|[!$&-.0-9;=@-Z_a-z~])+)(?:\/\g<segment>)*)|(?<path-empty>))(?:\?(?<query>[^#]*))?(?:\#(?<fragment>(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*))?)\z/
-
1
attr_reader :regexp
-
-
1
def initialize
-
1
@regexp = default_regexp.each_value(&:freeze).freeze
-
end
-
-
1
def split(uri) #:nodoc:
-
begin
-
28
uri = uri.to_str
-
rescue NoMethodError
-
raise InvalidURIError, "bad URI(is not URI?): #{uri.inspect}"
-
end
-
28
uri.ascii_only? or
-
raise InvalidURIError, "URI must be ascii only #{uri.dump}"
-
28
if m = RFC3986_URI.match(uri)
-
26
query = m["query".freeze]
-
26
scheme = m["scheme".freeze]
-
26
opaque = m["path-rootless".freeze]
-
26
if opaque
-
opaque << "?#{query}" if query
-
[ scheme,
-
nil, # userinfo
-
nil, # host
-
nil, # port
-
nil, # registry
-
nil, # path
-
opaque,
-
nil, # query
-
m["fragment".freeze]
-
]
-
else # normal
-
26
[ scheme,
-
m["userinfo".freeze],
-
m["host".freeze],
-
m["port".freeze],
-
nil, # registry
-
26
(m["path-abempty".freeze] ||
-
m["path-absolute".freeze] ||
-
m["path-empty".freeze]),
-
nil, # opaque
-
query,
-
m["fragment".freeze]
-
]
-
end
-
2
elsif m = RFC3986_relative_ref.match(uri)
-
2
[ nil, # scheme
-
m["userinfo".freeze],
-
m["host".freeze],
-
m["port".freeze],
-
nil, # registry,
-
2
(m["path-abempty".freeze] ||
-
m["path-absolute".freeze] ||
-
m["path-noscheme".freeze] ||
-
m["path-empty".freeze]),
-
nil, # opaque
-
m["query".freeze],
-
m["fragment".freeze]
-
]
-
else
-
raise InvalidURIError, "bad URI(is not URI?): #{uri.inspect}"
-
end
-
end
-
-
1
def parse(uri) # :nodoc:
-
scheme, userinfo, host, port,
-
2
registry, path, opaque, query, fragment = self.split(uri)
-
2
scheme_list = URI.scheme_list
-
2
if scheme && scheme_list.include?(uc = scheme.upcase)
-
scheme_list[uc].new(scheme, userinfo, host, port,
-
registry, path, opaque, query,
-
fragment, self)
-
else
-
2
Generic.new(scheme, userinfo, host, port,
-
registry, path, opaque, query,
-
fragment, self)
-
end
-
end
-
-
-
1
def join(*uris) # :nodoc:
-
uris[0] = convert_to_uri(uris[0])
-
uris.inject :merge
-
end
-
-
1
@@to_s = Kernel.instance_method(:to_s)
-
1
def inspect
-
@@to_s.bind_call(self)
-
end
-
-
1
private
-
-
1
def default_regexp # :nodoc:
-
1
{
-
SCHEME: /\A[A-Za-z][A-Za-z0-9+\-.]*\z/,
-
USERINFO: /\A(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*\z/,
-
HOST: /\A(?:(?<IP-literal>\[(?:(?<IPv6address>(?:\h{1,4}:){6}(?<ls32>\h{1,4}:\h{1,4}|(?<IPv4address>(?<dec-octet>[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g<dec-octet>\.\g<dec-octet>\.\g<dec-octet>))|::(?:\h{1,4}:){5}\g<ls32>|\h{,4}::(?:\h{1,4}:){4}\g<ls32>|(?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g<ls32>|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g<ls32>|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g<ls32>|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g<ls32>|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?<IPvFuture>v\h+\.[!$&-.0-;=A-Z_a-z~]+))\])|\g<IPv4address>|(?<reg-name>(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])*))\z/,
-
ABS_PATH: /\A\/(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*(?:\/(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*)*\z/,
-
REL_PATH: /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+(?:\/(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*)*\z/,
-
QUERY: /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*\z/,
-
FRAGMENT: /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*\z/,
-
OPAQUE: /\A(?:[^\/].*)?\z/,
-
PORT: /\A[\x09\x0a\x0c\x0d ]*\d*[\x09\x0a\x0c\x0d ]*\z/,
-
}
-
end
-
-
1
def convert_to_uri(uri)
-
if uri.is_a?(URI::Generic)
-
uri
-
elsif uri = String.try_convert(uri)
-
parse(uri)
-
else
-
raise ArgumentError,
-
"bad argument (expected URI object or URI string)"
-
end
-
end
-
-
end # class Parser
-
end # module URI
-
1
module URI
-
# :stopdoc:
-
1
VERSION_CODE = '001000'.freeze
-
4
VERSION = VERSION_CODE.scan(/../).collect{|n| n.to_i}.join('.').freeze
-
# :startdoc:
-
end
-
# frozen_string_literal: false
-
-
begin
-
1
require 'psych'
-
rescue LoadError
-
warn "It seems your ruby installation is missing psych (for YAML output).\n" \
-
"To eliminate this warning, please install libyaml and reinstall your ruby.\n",
-
uplevel: 1
-
raise
-
end
-
-
1
YAML = Psych # :nodoc:
-
-
# YAML Ain't Markup Language
-
#
-
# This module provides a Ruby interface for data serialization in YAML format.
-
#
-
# The YAML module is an alias of Psych, the YAML engine for Ruby.
-
#
-
# == Usage
-
#
-
# Working with YAML can be very simple, for example:
-
#
-
# require 'yaml'
-
# # Parse a YAML string
-
# YAML.load("--- foo") #=> "foo"
-
#
-
# # Emit some YAML
-
# YAML.dump("foo") # => "--- foo\n...\n"
-
# { :a => 'b'}.to_yaml # => "---\n:a: b\n"
-
#
-
# As the implementation is provided by the Psych library, detailed documentation
-
# can be found in that library's docs (also part of standard library).
-
#
-
# == Security
-
#
-
# Do not use YAML to load untrusted data. Doing so is unsafe and could allow
-
# malicious input to execute arbitrary code inside your application. Please see
-
# doc/security.rdoc for more information.
-
#
-
# == History
-
#
-
# Syck was the original for YAML implementation in Ruby's standard library
-
# developed by why the lucky stiff.
-
#
-
# You can still use Syck, if you prefer, for parsing and emitting YAML, but you
-
# must install the 'syck' gem now in order to use it.
-
#
-
# In older Ruby versions, ie. <= 1.9, Syck is still provided, however it was
-
# completely removed with the release of Ruby 2.0.0.
-
#
-
# == More info
-
#
-
# For more advanced details on the implementation see Psych, and also check out
-
# http://yaml.org for spec details and other helpful information.
-
#
-
# Psych is maintained by Aaron Patterson on github: https://github.com/ruby/psych
-
#
-
# Syck can also be found on github: https://github.com/ruby/syck
-
1
module YAML
-
end